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,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — 里程碑自动检测器
|
|
3
|
+
*
|
|
4
|
+
* 自动检测公司成就事件,写入 opc_celebrations 表。
|
|
5
|
+
* 检测逻辑确保同一成就只庆祝一次。
|
|
6
|
+
*
|
|
7
|
+
* 成就类型:
|
|
8
|
+
* - first_revenue 第一笔收入
|
|
9
|
+
* - revenue_10k 累计收入破万
|
|
10
|
+
* - revenue_50k 累计收入破5万
|
|
11
|
+
* - revenue_100k 累计收入破10万
|
|
12
|
+
* - revenue_500k 累计收入破50万
|
|
13
|
+
* - first_contract 第一份合同
|
|
14
|
+
* - first_customer 第一个客户
|
|
15
|
+
* - first_monthly_profit 首次月度盈利
|
|
16
|
+
* - project_completed 项目完成
|
|
17
|
+
* - canvas_complete 画布 100%
|
|
18
|
+
* - funding_closed 融资完成
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { OpcDatabase } from "../db/index.js";
|
|
22
|
+
|
|
23
|
+
type CelebrationRow = { id: string };
|
|
24
|
+
type CountRow = { cnt: number };
|
|
25
|
+
type SumRow = { total: number };
|
|
26
|
+
|
|
27
|
+
const OPB_CANVAS_FIELDS = [
|
|
28
|
+
"track", "target_customer", "pain_point", "solution",
|
|
29
|
+
"unique_value", "channels", "revenue_model", "cost_structure",
|
|
30
|
+
"key_resources", "key_activities", "key_partners", "unfair_advantage",
|
|
31
|
+
"metrics", "non_compete", "scaling_strategy", "notes",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/** 检查某种庆祝是否已存在(防重复) */
|
|
35
|
+
function celebrationExists(db: OpcDatabase, companyId: string, celebrationType: string): boolean {
|
|
36
|
+
const row = db.queryOne(
|
|
37
|
+
"SELECT id FROM opc_celebrations WHERE company_id = ? AND celebration_type = ?",
|
|
38
|
+
companyId, celebrationType,
|
|
39
|
+
) as CelebrationRow | null;
|
|
40
|
+
return row !== null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 写入庆祝记录 */
|
|
44
|
+
function createCelebration(
|
|
45
|
+
db: OpcDatabase,
|
|
46
|
+
companyId: string,
|
|
47
|
+
celebrationType: string,
|
|
48
|
+
title: string,
|
|
49
|
+
message: string,
|
|
50
|
+
metricValue: number,
|
|
51
|
+
): void {
|
|
52
|
+
db.execute(
|
|
53
|
+
`INSERT INTO opc_celebrations (id, company_id, celebration_type, title, message, metric_value, shown, created_at)
|
|
54
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, datetime('now'))`,
|
|
55
|
+
db.genId(), companyId, celebrationType, title, message, metricValue,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 检测收入相关里程碑 */
|
|
60
|
+
function checkRevenueMilestones(db: OpcDatabase, companyId: string, companyName: string, log: (msg: string) => void): void {
|
|
61
|
+
const totalIncome = (db.queryOne(
|
|
62
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'",
|
|
63
|
+
companyId,
|
|
64
|
+
) as SumRow).total;
|
|
65
|
+
|
|
66
|
+
if (totalIncome <= 0) return;
|
|
67
|
+
|
|
68
|
+
// 第一笔收入
|
|
69
|
+
if (!celebrationExists(db, companyId, "first_revenue")) {
|
|
70
|
+
createCelebration(db, companyId, "first_revenue",
|
|
71
|
+
"第一笔收入!",
|
|
72
|
+
`恭喜「${companyName}」获得了第一笔收入 ${totalIncome.toLocaleString()} 元!万事开头难,这是最重要的一步。`,
|
|
73
|
+
totalIncome,
|
|
74
|
+
);
|
|
75
|
+
log(`opc-milestone: [${companyName}] 🎉 第一笔收入 ${totalIncome.toLocaleString()} 元`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 收入阈值里程碑
|
|
79
|
+
const thresholds: [number, string, string][] = [
|
|
80
|
+
[10_000, "revenue_10k", "累计收入突破 1 万元!"],
|
|
81
|
+
[50_000, "revenue_50k", "累计收入突破 5 万元!"],
|
|
82
|
+
[100_000, "revenue_100k", "累计收入突破 10 万元!"],
|
|
83
|
+
[500_000, "revenue_500k", "累计收入突破 50 万元!"],
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
for (const [threshold, type, title] of thresholds) {
|
|
87
|
+
if (totalIncome >= threshold && !celebrationExists(db, companyId, type)) {
|
|
88
|
+
createCelebration(db, companyId, type, title,
|
|
89
|
+
`「${companyName}」累计收入达到 ${totalIncome.toLocaleString()} 元,突破 ${(threshold / 10000).toFixed(0)} 万元大关!`,
|
|
90
|
+
totalIncome,
|
|
91
|
+
);
|
|
92
|
+
log(`opc-milestone: [${companyName}] 🎉 ${title}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 检测首次月度盈利 */
|
|
98
|
+
function checkMonthlyProfit(db: OpcDatabase, companyId: string, companyName: string, log: (msg: string) => void): void {
|
|
99
|
+
if (celebrationExists(db, companyId, "first_monthly_profit")) return;
|
|
100
|
+
|
|
101
|
+
const row = db.queryOne(
|
|
102
|
+
`SELECT strftime('%Y-%m', transaction_date) as month,
|
|
103
|
+
SUM(CASE WHEN type='income' THEN amount ELSE 0 END) as income,
|
|
104
|
+
SUM(CASE WHEN type='expense' THEN amount ELSE 0 END) as expense
|
|
105
|
+
FROM opc_transactions
|
|
106
|
+
WHERE company_id = ?
|
|
107
|
+
GROUP BY month
|
|
108
|
+
HAVING income > 0 AND income > expense
|
|
109
|
+
LIMIT 1`,
|
|
110
|
+
companyId,
|
|
111
|
+
) as { month: string; income: number; expense: number } | null;
|
|
112
|
+
|
|
113
|
+
if (row) {
|
|
114
|
+
const profit = row.income - row.expense;
|
|
115
|
+
createCelebration(db, companyId, "first_monthly_profit",
|
|
116
|
+
"首次月度盈利!",
|
|
117
|
+
`「${companyName}」在 ${row.month} 首次实现月度盈利,净利润 ${profit.toLocaleString()} 元!`,
|
|
118
|
+
profit,
|
|
119
|
+
);
|
|
120
|
+
log(`opc-milestone: [${companyName}] 🎉 首次月度盈利 ${profit.toLocaleString()} 元`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 检测合同里程碑 */
|
|
125
|
+
function checkContractMilestones(db: OpcDatabase, companyId: string, companyName: string, log: (msg: string) => void): void {
|
|
126
|
+
if (celebrationExists(db, companyId, "first_contract")) return;
|
|
127
|
+
|
|
128
|
+
const cnt = (db.queryOne(
|
|
129
|
+
"SELECT COUNT(*) as cnt FROM opc_contracts WHERE company_id = ?",
|
|
130
|
+
companyId,
|
|
131
|
+
) as CountRow).cnt;
|
|
132
|
+
|
|
133
|
+
if (cnt > 0) {
|
|
134
|
+
createCelebration(db, companyId, "first_contract",
|
|
135
|
+
"第一份合同!",
|
|
136
|
+
`「${companyName}」签订了第一份正式合同!业务正在走上正轨。`,
|
|
137
|
+
cnt,
|
|
138
|
+
);
|
|
139
|
+
log(`opc-milestone: [${companyName}] 🎉 第一份合同`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 检测客户里程碑 */
|
|
144
|
+
function checkCustomerMilestones(db: OpcDatabase, companyId: string, companyName: string, log: (msg: string) => void): void {
|
|
145
|
+
if (celebrationExists(db, companyId, "first_customer")) return;
|
|
146
|
+
|
|
147
|
+
const cnt = (db.queryOne(
|
|
148
|
+
"SELECT COUNT(*) as cnt FROM opc_contacts WHERE company_id = ?",
|
|
149
|
+
companyId,
|
|
150
|
+
) as CountRow).cnt;
|
|
151
|
+
|
|
152
|
+
if (cnt > 0) {
|
|
153
|
+
createCelebration(db, companyId, "first_customer",
|
|
154
|
+
"客户池开始建立!",
|
|
155
|
+
`「${companyName}」添加了第一个客户/联系人,客户池正在建立中。`,
|
|
156
|
+
cnt,
|
|
157
|
+
);
|
|
158
|
+
log(`opc-milestone: [${companyName}] 🎉 第一个客户`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 检测项目完成里程碑 */
|
|
163
|
+
function checkProjectCompleted(db: OpcDatabase, companyId: string, companyName: string, log: (msg: string) => void): void {
|
|
164
|
+
// 找到近期完成的项目且尚未庆祝的
|
|
165
|
+
const completedProjects = db.query(
|
|
166
|
+
`SELECT id, name FROM opc_projects
|
|
167
|
+
WHERE company_id = ? AND status = 'completed'`,
|
|
168
|
+
companyId,
|
|
169
|
+
) as { id: string; name: string }[];
|
|
170
|
+
|
|
171
|
+
for (const proj of completedProjects) {
|
|
172
|
+
const cType = `project_completed_${proj.id}`;
|
|
173
|
+
if (celebrationExists(db, companyId, cType)) continue;
|
|
174
|
+
|
|
175
|
+
createCelebration(db, companyId, cType,
|
|
176
|
+
`项目完成:${proj.name}`,
|
|
177
|
+
`「${companyName}」的项目「${proj.name}」顺利完成!`,
|
|
178
|
+
1,
|
|
179
|
+
);
|
|
180
|
+
log(`opc-milestone: [${companyName}] 🎉 项目完成: ${proj.name}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 检测画布完成度 */
|
|
185
|
+
function checkCanvasComplete(db: OpcDatabase, companyId: string, companyName: string, log: (msg: string) => void): void {
|
|
186
|
+
if (celebrationExists(db, companyId, "canvas_complete")) return;
|
|
187
|
+
|
|
188
|
+
const canvas = db.queryOne(
|
|
189
|
+
"SELECT * FROM opc_opb_canvas WHERE company_id = ?",
|
|
190
|
+
companyId,
|
|
191
|
+
) as Record<string, string> | null;
|
|
192
|
+
|
|
193
|
+
if (!canvas) return;
|
|
194
|
+
|
|
195
|
+
const filled = OPB_CANVAS_FIELDS.filter(f => canvas[f] && canvas[f].trim() !== "").length;
|
|
196
|
+
if (filled >= OPB_CANVAS_FIELDS.length) {
|
|
197
|
+
createCelebration(db, companyId, "canvas_complete",
|
|
198
|
+
"商业画布规划完成!",
|
|
199
|
+
`「${companyName}」的 OPB 商业画布 ${OPB_CANVAS_FIELDS.length} 个字段已全部填写完成!商业模式规划圆满。`,
|
|
200
|
+
OPB_CANVAS_FIELDS.length,
|
|
201
|
+
);
|
|
202
|
+
log(`opc-milestone: [${companyName}] 🎉 OPB 画布完成`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 检测融资完成 */
|
|
207
|
+
function checkFundingClosed(db: OpcDatabase, companyId: string, companyName: string, log: (msg: string) => void): void {
|
|
208
|
+
const closedRounds = db.query(
|
|
209
|
+
"SELECT id, round_name, amount FROM opc_investment_rounds WHERE company_id = ? AND status = 'closed'",
|
|
210
|
+
companyId,
|
|
211
|
+
) as { id: string; round_name: string; amount: number }[];
|
|
212
|
+
|
|
213
|
+
for (const round of closedRounds) {
|
|
214
|
+
const cType = `funding_closed_${round.id}`;
|
|
215
|
+
if (celebrationExists(db, companyId, cType)) continue;
|
|
216
|
+
|
|
217
|
+
createCelebration(db, companyId, cType,
|
|
218
|
+
`融资完成:${round.round_name}`,
|
|
219
|
+
`「${companyName}」的${round.round_name}成功关闭,融资金额 ${round.amount.toLocaleString()} 元!`,
|
|
220
|
+
round.amount,
|
|
221
|
+
);
|
|
222
|
+
log(`opc-milestone: [${companyName}] 🎉 融资完成: ${round.round_name}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 检测单个公司的所有里程碑 */
|
|
227
|
+
export function detectMilestones(db: OpcDatabase, companyId: string, log: (msg: string) => void): void {
|
|
228
|
+
try {
|
|
229
|
+
const company = db.getCompany(companyId);
|
|
230
|
+
if (!company) return;
|
|
231
|
+
const name = company.name;
|
|
232
|
+
|
|
233
|
+
checkRevenueMilestones(db, companyId, name, log);
|
|
234
|
+
checkMonthlyProfit(db, companyId, name, log);
|
|
235
|
+
checkContractMilestones(db, companyId, name, log);
|
|
236
|
+
checkCustomerMilestones(db, companyId, name, log);
|
|
237
|
+
checkProjectCompleted(db, companyId, name, log);
|
|
238
|
+
checkCanvasComplete(db, companyId, name, log);
|
|
239
|
+
checkFundingClosed(db, companyId, name, log);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
log(`opc-milestone: 检测异常 [${companyId}]: ${err instanceof Error ? err.message : String(err)}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** 检测所有公司的里程碑 */
|
|
246
|
+
export function detectMilestonesForAll(db: OpcDatabase, log: (msg: string) => void): void {
|
|
247
|
+
const companies = db.query("SELECT id FROM opc_companies") as { id: string }[];
|
|
248
|
+
for (const c of companies) {
|
|
249
|
+
detectMilestones(db, c.id, log);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — 主动智能后台服务
|
|
3
|
+
*
|
|
4
|
+
* 统一调度所有后台扫描任务,替代单独的 startReminderService。
|
|
5
|
+
* 30 秒后首次扫描,之后每小时执行:
|
|
6
|
+
* 1. runReminderScan() — 已有的 4 项检查(税务、合同、现金流、融资)
|
|
7
|
+
* 2. recalculateAllStages() — 阶段检测
|
|
8
|
+
* 3. runIntelligenceScan() — 智能洞察分析
|
|
9
|
+
* 4. detectMilestonesForAll() — 里程碑检测
|
|
10
|
+
* 5. expireStaleInsights() — 清理过期洞察
|
|
11
|
+
* 6. saveDailyBriefings() — 保存每日简报快照
|
|
12
|
+
* 7. createScheduledStaffTasks() — 创建定时员工任务(pending,等 AI 触发)
|
|
13
|
+
* 8. reapStaleTasks() — 清理超时的 in_progress 任务(subagent 异常未被钩子捕获时的兜底)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { OpcDatabase } from "../db/index.js";
|
|
17
|
+
import { runReminderScan } from "./reminder-service.js";
|
|
18
|
+
import { runIntelligenceScan, expireStaleInsights } from "./intelligence-engine.js";
|
|
19
|
+
import { detectMilestonesForAll } from "./milestone-detector.js";
|
|
20
|
+
import { recalculateAllStages } from "./stage-detector.js";
|
|
21
|
+
import { saveDailyBriefing } from "./briefing-builder.js";
|
|
22
|
+
import { TaskExecutor } from "./task-executor.js";
|
|
23
|
+
import { alertsToStaffTasks, insightsToStaffTasks } from "./event-triggers.js";
|
|
24
|
+
|
|
25
|
+
/** 为所有公司保存每日简报快照 */
|
|
26
|
+
function saveDailyBriefings(db: OpcDatabase, log: (msg: string) => void): void {
|
|
27
|
+
const companies = db.query("SELECT id FROM opc_companies") as { id: string }[];
|
|
28
|
+
let saved = 0;
|
|
29
|
+
for (const c of companies) {
|
|
30
|
+
try {
|
|
31
|
+
saveDailyBriefing(db, c.id);
|
|
32
|
+
saved++;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
log(`opc-proactive: 保存简报失败 [${c.id}]: ${err instanceof Error ? err.message : String(err)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (saved > 0) log(`opc-proactive: 已保存 ${saved} 家公司的每日简报快照`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 创建定时员工任务(pending 状态,等 AI 触发执行) */
|
|
41
|
+
function createScheduledStaffTasks(db: OpcDatabase, log: (msg: string) => void): void {
|
|
42
|
+
const executor = new TaskExecutor(db);
|
|
43
|
+
|
|
44
|
+
// 检查今日是否已创建过 daily 任务
|
|
45
|
+
const lastDailyRun = db.queryOne(
|
|
46
|
+
"SELECT value FROM opc_tool_config WHERE key = 'last_daily_task_run'",
|
|
47
|
+
) as { value: string } | null;
|
|
48
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
49
|
+
|
|
50
|
+
if (!lastDailyRun || lastDailyRun.value !== today) {
|
|
51
|
+
const dailyCount = executor.createPendingScheduledTasks("daily");
|
|
52
|
+
if (dailyCount > 0) {
|
|
53
|
+
log(`opc-executor: 已创建 ${dailyCount} 个每日定时任务(pending)`);
|
|
54
|
+
}
|
|
55
|
+
db.execute(
|
|
56
|
+
`INSERT INTO opc_tool_config (key, value) VALUES ('last_daily_task_run', ?)
|
|
57
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
|
58
|
+
today,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 每周一创建 weekly 任务
|
|
63
|
+
const dayOfWeek = new Date().getDay();
|
|
64
|
+
if (dayOfWeek === 1) {
|
|
65
|
+
const lastWeeklyRun = db.queryOne(
|
|
66
|
+
"SELECT value FROM opc_tool_config WHERE key = 'last_weekly_task_run'",
|
|
67
|
+
) as { value: string } | null;
|
|
68
|
+
if (!lastWeeklyRun || lastWeeklyRun.value !== today) {
|
|
69
|
+
const weeklyCount = executor.createPendingScheduledTasks("weekly");
|
|
70
|
+
if (weeklyCount > 0) {
|
|
71
|
+
log(`opc-executor: 已创建 ${weeklyCount} 个每周定时任务(pending)`);
|
|
72
|
+
}
|
|
73
|
+
db.execute(
|
|
74
|
+
`INSERT INTO opc_tool_config (key, value) VALUES ('last_weekly_task_run', ?)
|
|
75
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
|
76
|
+
today,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 清理超过 2 小时仍 in_progress 且有 session_key 的任务(subagent_ended 未触发时的兜底) */
|
|
83
|
+
function reapStaleTasks(db: OpcDatabase, log: (msg: string) => void): void {
|
|
84
|
+
try {
|
|
85
|
+
const staleTasks = db.query(
|
|
86
|
+
`SELECT id, company_id, staff_role, title FROM opc_staff_tasks
|
|
87
|
+
WHERE status = 'in_progress'
|
|
88
|
+
AND session_key != ''
|
|
89
|
+
AND started_at != ''
|
|
90
|
+
AND started_at < datetime('now', '-2 hours')`,
|
|
91
|
+
) as { id: string; company_id: string; staff_role: string; title: string }[];
|
|
92
|
+
|
|
93
|
+
if (staleTasks.length === 0) return;
|
|
94
|
+
|
|
95
|
+
const now = new Date().toISOString();
|
|
96
|
+
for (const task of staleTasks) {
|
|
97
|
+
db.execute(
|
|
98
|
+
`UPDATE opc_staff_tasks SET status = 'cancelled', completed_at = ?,
|
|
99
|
+
result_summary = '[系统] 任务执行超时(超过2小时未完成),已自动取消'
|
|
100
|
+
WHERE id = ? AND status = 'in_progress'`,
|
|
101
|
+
now, task.id,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
log(`opc-proactive: 已清理 ${staleTasks.length} 个超时任务(in_progress 超过2小时)`);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
log(`opc-proactive: 清理超时任务异常: ${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 执行一次完整的主动扫描 */
|
|
112
|
+
function runFullScan(db: OpcDatabase, log: (msg: string) => void, webhookUrl?: string): void {
|
|
113
|
+
try {
|
|
114
|
+
log("opc-proactive: 开始全量扫描...");
|
|
115
|
+
|
|
116
|
+
// 1. 原有提醒扫描
|
|
117
|
+
runReminderScan(db, log, webhookUrl);
|
|
118
|
+
|
|
119
|
+
// 2. 阶段检测(先于洞察分析,因为 generateNextSteps 依赖阶段)
|
|
120
|
+
recalculateAllStages(db, log);
|
|
121
|
+
|
|
122
|
+
// 3. 智能洞察分析
|
|
123
|
+
runIntelligenceScan(db, log);
|
|
124
|
+
|
|
125
|
+
// 4. 里程碑检测
|
|
126
|
+
detectMilestonesForAll(db, log);
|
|
127
|
+
|
|
128
|
+
// 5. 清理过期洞察
|
|
129
|
+
expireStaleInsights(db, log);
|
|
130
|
+
|
|
131
|
+
// 6. 保存每日简报快照
|
|
132
|
+
saveDailyBriefings(db, log);
|
|
133
|
+
|
|
134
|
+
// 7. 创建定时员工任务
|
|
135
|
+
createScheduledStaffTasks(db, log);
|
|
136
|
+
|
|
137
|
+
// 8. 清理超时的 in_progress 任务
|
|
138
|
+
reapStaleTasks(db, log);
|
|
139
|
+
|
|
140
|
+
// 9. 将新告警自动分配给对应 AI 员工
|
|
141
|
+
alertsToStaffTasks(db, log);
|
|
142
|
+
|
|
143
|
+
// 10. 将可执行洞察转化为员工待办
|
|
144
|
+
insightsToStaffTasks(db, log);
|
|
145
|
+
|
|
146
|
+
log("opc-proactive: 全量扫描完成");
|
|
147
|
+
} catch (err) {
|
|
148
|
+
log(`opc-proactive: 扫描异常: ${err instanceof Error ? err.message : String(err)}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 启动主动智能后台服务,返回停止函数。
|
|
154
|
+
* 30 秒后首次扫描,之后每小时重复。
|
|
155
|
+
*/
|
|
156
|
+
export function startProactiveService(
|
|
157
|
+
db: OpcDatabase,
|
|
158
|
+
log: (msg: string) => void,
|
|
159
|
+
webhookUrl?: string,
|
|
160
|
+
intervalMs = 3600_000,
|
|
161
|
+
): () => void {
|
|
162
|
+
// 延迟 30 秒首次扫描
|
|
163
|
+
log("opc-proactive: 首次全量扫描将在 30 秒后启动(阶段检测 + 智能洞察 + 里程碑检测)");
|
|
164
|
+
const initTimer = setTimeout(() => {
|
|
165
|
+
log("opc-proactive: 首次全量扫描启动");
|
|
166
|
+
runFullScan(db, log, webhookUrl);
|
|
167
|
+
}, 30_000);
|
|
168
|
+
|
|
169
|
+
// 每小时周期性扫描
|
|
170
|
+
const interval = setInterval(() => {
|
|
171
|
+
runFullScan(db, log, webhookUrl);
|
|
172
|
+
}, intervalMs);
|
|
173
|
+
|
|
174
|
+
return () => {
|
|
175
|
+
clearTimeout(initTimer);
|
|
176
|
+
clearInterval(interval);
|
|
177
|
+
log("opc-proactive: 主动智能服务已停止");
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
* 防重复:同一公司同一类别同一周期内不重复写入。
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import https from "node:https";
|
|
13
|
-
import http from "node:http";
|
|
14
12
|
import type { OpcDatabase } from "../db/index.js";
|
|
15
13
|
|
|
16
14
|
type AlertRow = { id: string; company_id: string; category: string; title: string };
|
|
@@ -68,42 +66,9 @@ function createAlert(db: OpcDatabase, params: {
|
|
|
68
66
|
);
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
/** 向飞书/企业微信 Webhook 推送一条文本消息(fire-and-forget) */
|
|
72
|
-
function sendWebhook(url: string, text: string, log: (msg: string) => void): void {
|
|
73
|
-
try {
|
|
74
|
-
const isFeishu = url.includes("feishu.cn") || url.includes("larksuite.com");
|
|
75
|
-
const body = isFeishu
|
|
76
|
-
? JSON.stringify({ msg_type: "text", content: { text } })
|
|
77
|
-
: JSON.stringify({ msgtype: "text", text: { content: text } });
|
|
78
|
-
|
|
79
|
-
const parsed = new URL(url);
|
|
80
|
-
const transport = parsed.protocol === "https:" ? https : http;
|
|
81
|
-
const req = transport.request(
|
|
82
|
-
{
|
|
83
|
-
hostname: parsed.hostname,
|
|
84
|
-
port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
|
|
85
|
-
path: parsed.pathname + parsed.search,
|
|
86
|
-
method: "POST",
|
|
87
|
-
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
88
|
-
},
|
|
89
|
-
(res) => {
|
|
90
|
-
res.resume(); // drain response
|
|
91
|
-
if (res.statusCode && res.statusCode >= 400) {
|
|
92
|
-
log(`opc-reminder: Webhook 响应异常 (${res.statusCode})`);
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
);
|
|
96
|
-
req.on("error", (err) => log(`opc-reminder: Webhook 请求失败: ${err.message}`));
|
|
97
|
-
req.write(body);
|
|
98
|
-
req.end();
|
|
99
|
-
} catch (err) {
|
|
100
|
-
log(`opc-reminder: Webhook 发送异常: ${err instanceof Error ? err.message : String(err)}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
69
|
// ── 检查项 1:税务申报到期提醒 ────────────────────────────────
|
|
105
70
|
|
|
106
|
-
function checkTaxDeadlines(db: OpcDatabase, log: (msg: string) => void,
|
|
71
|
+
function checkTaxDeadlines(db: OpcDatabase, log: (msg: string) => void, _webhookUrl?: string): number {
|
|
107
72
|
let count = 0;
|
|
108
73
|
const rows = db.query(
|
|
109
74
|
`SELECT t.*, c.name as company_name FROM opc_tax_filings t
|
|
@@ -130,14 +95,13 @@ function checkTaxDeadlines(db: OpcDatabase, log: (msg: string) => void, webhookU
|
|
|
130
95
|
createAlert(db, { companyId: row.company_id, title, message, severity, category: "tax" });
|
|
131
96
|
count++;
|
|
132
97
|
log(`opc-reminder: 税务提醒 [${row.company_name}] ${title}`);
|
|
133
|
-
if (webhookUrl) sendWebhook(webhookUrl, `【税务提醒】${title}\n${message}`, log);
|
|
134
98
|
}
|
|
135
99
|
return count;
|
|
136
100
|
}
|
|
137
101
|
|
|
138
102
|
// ── 检查项 2:合同到期提醒 ─────────────────────────────────────
|
|
139
103
|
|
|
140
|
-
function checkContractExpiry(db: OpcDatabase, log: (msg: string) => void,
|
|
104
|
+
function checkContractExpiry(db: OpcDatabase, log: (msg: string) => void, _webhookUrl?: string): number {
|
|
141
105
|
let count = 0;
|
|
142
106
|
const rows = db.query(
|
|
143
107
|
`SELECT t.*, c.name as company_name FROM opc_contracts t
|
|
@@ -164,14 +128,13 @@ function checkContractExpiry(db: OpcDatabase, log: (msg: string) => void, webhoo
|
|
|
164
128
|
createAlert(db, { companyId: row.company_id, title, message, severity, category: "contract" });
|
|
165
129
|
count++;
|
|
166
130
|
log(`opc-reminder: 合同提醒 [${row.company_name}] ${title}`);
|
|
167
|
-
if (webhookUrl) sendWebhook(webhookUrl, `【合同提醒】${title}\n${message}`, log);
|
|
168
131
|
}
|
|
169
132
|
return count;
|
|
170
133
|
}
|
|
171
134
|
|
|
172
135
|
// ── 检查项 3:现金流预警 ──────────────────────────────────────
|
|
173
136
|
|
|
174
|
-
function checkCashFlow(db: OpcDatabase, log: (msg: string) => void,
|
|
137
|
+
function checkCashFlow(db: OpcDatabase, log: (msg: string) => void, _webhookUrl?: string): number {
|
|
175
138
|
let count = 0;
|
|
176
139
|
const start = daysAgo(30);
|
|
177
140
|
const companies = db.query(
|
|
@@ -207,14 +170,13 @@ function checkCashFlow(db: OpcDatabase, log: (msg: string) => void, webhookUrl?:
|
|
|
207
170
|
});
|
|
208
171
|
count++;
|
|
209
172
|
log(`opc-reminder: 现金流预警 [${company.name}] 净流出 ${Math.abs(net).toLocaleString()} 元`);
|
|
210
|
-
if (webhookUrl) sendWebhook(webhookUrl, `【现金流预警】${company.name} 近30天净流出 ${Math.abs(net).toLocaleString()} 元,请及时关注资金状况。`, log);
|
|
211
173
|
}
|
|
212
174
|
return count;
|
|
213
175
|
}
|
|
214
176
|
|
|
215
177
|
// ── 检查项 4:投资轮次跟进提醒 ──────────────────────────────────
|
|
216
178
|
|
|
217
|
-
function checkInvestmentRounds(db: OpcDatabase, log: (msg: string) => void,
|
|
179
|
+
function checkInvestmentRounds(db: OpcDatabase, log: (msg: string) => void, _webhookUrl?: string): number {
|
|
218
180
|
let count = 0;
|
|
219
181
|
// 找出 close_date 在7天内的活跃融资轮
|
|
220
182
|
const rows = db.query(
|
|
@@ -241,7 +203,6 @@ function checkInvestmentRounds(db: OpcDatabase, log: (msg: string) => void, webh
|
|
|
241
203
|
createAlert(db, { companyId: row.company_id, title, message, severity, category: "investment" });
|
|
242
204
|
count++;
|
|
243
205
|
log(`opc-reminder: 融资提醒 [${row.company_name}] ${title}`);
|
|
244
|
-
if (webhookUrl) sendWebhook(webhookUrl, `【融资提醒】${title}\n${message}`, log);
|
|
245
206
|
}
|
|
246
207
|
return count;
|
|
247
208
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — 会话-任务映射管理器
|
|
3
|
+
*
|
|
4
|
+
* 追踪 sessions_spawn 创建的子会话与 OPC 任务之间的对应关系。
|
|
5
|
+
* 当 subagent_ended 触发时,通过 childSessionKey 查找对应任务并自动更新状态。
|
|
6
|
+
*
|
|
7
|
+
* 内存 Map 作为热缓存,数据库 session_key 列作为持久化。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface SessionTaskMapping {
|
|
11
|
+
taskId: string;
|
|
12
|
+
companyId: string;
|
|
13
|
+
staffRole: string;
|
|
14
|
+
title: string;
|
|
15
|
+
runId?: string;
|
|
16
|
+
spawnedAt: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 内存映射:childSessionKey → 任务信息 */
|
|
20
|
+
const sessionMap = new Map<string, SessionTaskMapping>();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 注册一个 spawn 出的子会话与任务的映射关系。
|
|
24
|
+
*/
|
|
25
|
+
export function registerSpawnedSession(
|
|
26
|
+
childSessionKey: string,
|
|
27
|
+
mapping: SessionTaskMapping,
|
|
28
|
+
): void {
|
|
29
|
+
sessionMap.set(childSessionKey, mapping);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 根据 childSessionKey 查找对应的任务映射。
|
|
34
|
+
*/
|
|
35
|
+
export function getSessionTaskMapping(
|
|
36
|
+
childSessionKey: string,
|
|
37
|
+
): SessionTaskMapping | undefined {
|
|
38
|
+
return sessionMap.get(childSessionKey);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 移除已完成/已取消的会话映射。
|
|
43
|
+
*/
|
|
44
|
+
export function removeSessionTaskMapping(childSessionKey: string): void {
|
|
45
|
+
sessionMap.delete(childSessionKey);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 获取当前所有活跃的会话映射数量(用于调试/监控)。
|
|
50
|
+
*/
|
|
51
|
+
export function getActiveSessionCount(): number {
|
|
52
|
+
return sessionMap.size;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 清理所有映射(服务停止时调用)。
|
|
57
|
+
*/
|
|
58
|
+
export function clearAllSessionMappings(): void {
|
|
59
|
+
sessionMap.clear();
|
|
60
|
+
}
|