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,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — /opc 快捷命令
|
|
3
|
+
*
|
|
4
|
+
* 通过 registerCommand() 注册,用户输入 /opc 直接返回面板,
|
|
5
|
+
* 不经 LLM,毫秒级响应。
|
|
6
|
+
*
|
|
7
|
+
* 子命令:
|
|
8
|
+
* /opc 总览仪表盘
|
|
9
|
+
* /opc tasks 任务板
|
|
10
|
+
* /opc brief 今日简报
|
|
11
|
+
* /opc staff 员工状态
|
|
12
|
+
* /opc alerts 活跃告警
|
|
13
|
+
* /opc company <id> 公司详情
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
17
|
+
import type { OpcDatabase } from "../db/index.js";
|
|
18
|
+
import { computeHealthScore, computeGrowthScorecard } from "../opc/briefing-builder.js";
|
|
19
|
+
|
|
20
|
+
type CountRow = { cnt: number };
|
|
21
|
+
type SumRow = { total: number };
|
|
22
|
+
|
|
23
|
+
export function registerOpcCommand(api: OpenClawPluginApi, db: OpcDatabase): void {
|
|
24
|
+
api.registerCommand({
|
|
25
|
+
name: "opc",
|
|
26
|
+
description: "OPC 一人公司快捷面板(/opc, /opc tasks, /opc brief, /opc staff, /opc alerts)",
|
|
27
|
+
acceptsArgs: true,
|
|
28
|
+
handler(ctx: Record<string, unknown>) {
|
|
29
|
+
const args = ((ctx as { args?: string }).args ?? "").trim();
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
if (!args || args === "status") return renderDashboard(db);
|
|
33
|
+
if (args === "tasks") return renderTaskBoard(db);
|
|
34
|
+
if (args === "brief") return renderBriefing(db);
|
|
35
|
+
if (args === "staff") return renderStaffStatus(db);
|
|
36
|
+
if (args === "alerts") return renderAlerts(db);
|
|
37
|
+
if (args.startsWith("company ")) return renderCompanyDetail(db, args.slice(8).trim());
|
|
38
|
+
|
|
39
|
+
return { text: "用法: /opc [tasks|brief|staff|alerts|company <id>]" };
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return { text: `错误: ${err instanceof Error ? err.message : String(err)}` };
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
api.logger.info("opc: 已注册 /opc 快捷命令");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── 总览仪表盘 ──────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
function renderDashboard(db: OpcDatabase): { text: string } {
|
|
52
|
+
const companies = db.query(
|
|
53
|
+
"SELECT id, name, status FROM opc_companies ORDER BY created_at DESC LIMIT 10",
|
|
54
|
+
) as { id: string; name: string; status: string }[];
|
|
55
|
+
|
|
56
|
+
if (companies.length === 0) {
|
|
57
|
+
return { text: "📊 **OPC 仪表盘**\n\n暂无公司。使用 opc_manage register_company 注册第一家公司。" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const lines: string[] = ["📊 **OPC 仪表盘**", ""];
|
|
61
|
+
|
|
62
|
+
// 组合级汇总
|
|
63
|
+
let totalIncome = 0;
|
|
64
|
+
let totalExpense = 0;
|
|
65
|
+
for (const c of companies) {
|
|
66
|
+
totalIncome += (db.queryOne(
|
|
67
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'", c.id,
|
|
68
|
+
) as SumRow).total;
|
|
69
|
+
totalExpense += (db.queryOne(
|
|
70
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'expense'", c.id,
|
|
71
|
+
) as SumRow).total;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
lines.push(`**组合**: ${companies.length} 家公司 | 总收入: ${totalIncome.toLocaleString()} 元 | 总支出: ${totalExpense.toLocaleString()} 元 | 净利润: ${(totalIncome - totalExpense).toLocaleString()} 元`);
|
|
75
|
+
lines.push("");
|
|
76
|
+
|
|
77
|
+
// 每家公司一行
|
|
78
|
+
for (const c of companies) {
|
|
79
|
+
const health = computeHealthScore(db, c.id);
|
|
80
|
+
const stageRow = db.queryOne(
|
|
81
|
+
"SELECT stage_label FROM opc_company_stage WHERE company_id = ?", c.id,
|
|
82
|
+
) as { stage_label: string } | null;
|
|
83
|
+
const alertCount = (db.queryOne(
|
|
84
|
+
"SELECT COUNT(*) as cnt FROM opc_alerts WHERE company_id = ? AND status = 'active'", c.id,
|
|
85
|
+
) as CountRow).cnt;
|
|
86
|
+
const pendingTasks = (db.queryOne(
|
|
87
|
+
"SELECT COUNT(*) as cnt FROM opc_staff_tasks WHERE company_id = ? AND status IN ('pending', 'in_progress', 'pending_approval')", c.id,
|
|
88
|
+
) as CountRow).cnt;
|
|
89
|
+
|
|
90
|
+
const emoji = health.total >= 80 ? "🟢" : health.total >= 60 ? "🟡" : health.total >= 40 ? "🟠" : "🔴";
|
|
91
|
+
const badges: string[] = [];
|
|
92
|
+
if (alertCount > 0) badges.push(`⚠️${alertCount}`);
|
|
93
|
+
if (pendingTasks > 0) badges.push(`📋${pendingTasks}`);
|
|
94
|
+
const badgeStr = badges.length > 0 ? ` ${badges.join(" ")}` : "";
|
|
95
|
+
|
|
96
|
+
lines.push(`${emoji} **${c.name}** | ${stageRow?.stage_label ?? "未检测"} | ${health.total}分${badgeStr}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 紧急事项
|
|
100
|
+
const urgentAlerts = db.query(
|
|
101
|
+
`SELECT a.title, c.name as company_name FROM opc_alerts a
|
|
102
|
+
JOIN opc_companies c ON a.company_id = c.id
|
|
103
|
+
WHERE a.status = 'active' AND a.severity IN ('critical', 'warning')
|
|
104
|
+
ORDER BY CASE a.severity WHEN 'critical' THEN 1 ELSE 2 END
|
|
105
|
+
LIMIT 3`,
|
|
106
|
+
) as { title: string; company_name: string }[];
|
|
107
|
+
|
|
108
|
+
if (urgentAlerts.length > 0) {
|
|
109
|
+
lines.push("");
|
|
110
|
+
lines.push("**紧急事项**:");
|
|
111
|
+
for (const a of urgentAlerts) {
|
|
112
|
+
lines.push(`- [${a.company_name}] ${a.title}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 待决策事项
|
|
117
|
+
const pendingApproval = db.query(
|
|
118
|
+
`SELECT t.title, t.staff_role, c.name as company_name FROM opc_staff_tasks t
|
|
119
|
+
JOIN opc_companies c ON t.company_id = c.id
|
|
120
|
+
WHERE t.status = 'pending_approval'
|
|
121
|
+
ORDER BY t.created_at DESC LIMIT 3`,
|
|
122
|
+
) as { title: string; staff_role: string; company_name: string }[];
|
|
123
|
+
|
|
124
|
+
if (pendingApproval.length > 0) {
|
|
125
|
+
lines.push("");
|
|
126
|
+
lines.push("**待决策**:");
|
|
127
|
+
for (const t of pendingApproval) {
|
|
128
|
+
lines.push(`- [${t.company_name}/${t.staff_role}] ${t.title}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
lines.push("");
|
|
133
|
+
lines.push("子命令: `/opc tasks` `/opc brief` `/opc staff` `/opc alerts` `/opc company <id>`");
|
|
134
|
+
|
|
135
|
+
return { text: lines.join("\n") };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── 任务板 ──────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
function renderTaskBoard(db: OpcDatabase): { text: string } {
|
|
141
|
+
const lines: string[] = ["📋 **任务板**", ""];
|
|
142
|
+
|
|
143
|
+
// 待决策
|
|
144
|
+
const approvalTasks = db.query(
|
|
145
|
+
`SELECT t.id, t.title, t.staff_role, t.created_at, c.name as company_name,
|
|
146
|
+
s.role_name FROM opc_staff_tasks t
|
|
147
|
+
JOIN opc_companies c ON t.company_id = c.id
|
|
148
|
+
LEFT JOIN opc_staff_config s ON t.company_id = s.company_id AND t.staff_role = s.role
|
|
149
|
+
WHERE t.status = 'pending_approval'
|
|
150
|
+
ORDER BY t.created_at DESC LIMIT 5`,
|
|
151
|
+
) as { id: string; title: string; staff_role: string; created_at: string; company_name: string; role_name: string }[];
|
|
152
|
+
|
|
153
|
+
if (approvalTasks.length > 0) {
|
|
154
|
+
lines.push("⚠️ **需要决策**:");
|
|
155
|
+
for (const t of approvalTasks) {
|
|
156
|
+
lines.push(`- [${t.company_name}/${t.role_name ?? t.staff_role}] ${t.title}`);
|
|
157
|
+
}
|
|
158
|
+
lines.push("");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 进行中
|
|
162
|
+
const inProgress = db.query(
|
|
163
|
+
`SELECT t.title, t.staff_role, c.name as company_name,
|
|
164
|
+
s.role_name FROM opc_staff_tasks t
|
|
165
|
+
JOIN opc_companies c ON t.company_id = c.id
|
|
166
|
+
LEFT JOIN opc_staff_config s ON t.company_id = s.company_id AND t.staff_role = s.role
|
|
167
|
+
WHERE t.status = 'in_progress'
|
|
168
|
+
ORDER BY t.started_at DESC LIMIT 5`,
|
|
169
|
+
) as { title: string; staff_role: string; company_name: string; role_name: string }[];
|
|
170
|
+
|
|
171
|
+
if (inProgress.length > 0) {
|
|
172
|
+
lines.push("🔄 **进行中**:");
|
|
173
|
+
for (const t of inProgress) {
|
|
174
|
+
lines.push(`- [${t.company_name}/${t.role_name ?? t.staff_role}] ${t.title}`);
|
|
175
|
+
}
|
|
176
|
+
lines.push("");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 待执行
|
|
180
|
+
const pending = db.query(
|
|
181
|
+
`SELECT t.title, t.staff_role, t.priority, c.name as company_name,
|
|
182
|
+
s.role_name FROM opc_staff_tasks t
|
|
183
|
+
JOIN opc_companies c ON t.company_id = c.id
|
|
184
|
+
LEFT JOIN opc_staff_config s ON t.company_id = s.company_id AND t.staff_role = s.role
|
|
185
|
+
WHERE t.status = 'pending'
|
|
186
|
+
ORDER BY CASE t.priority WHEN 'urgent' THEN 1 WHEN 'high' THEN 2 ELSE 3 END, t.created_at DESC
|
|
187
|
+
LIMIT 10`,
|
|
188
|
+
) as { title: string; staff_role: string; priority: string; company_name: string; role_name: string }[];
|
|
189
|
+
|
|
190
|
+
if (pending.length > 0) {
|
|
191
|
+
lines.push("⏳ **待执行**:");
|
|
192
|
+
for (const t of pending) {
|
|
193
|
+
const pri = t.priority === "urgent" ? " [紧急]" : t.priority === "high" ? " [重要]" : "";
|
|
194
|
+
lines.push(`- [${t.company_name}/${t.role_name ?? t.staff_role}] ${t.title}${pri}`);
|
|
195
|
+
}
|
|
196
|
+
lines.push("");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 最近完成
|
|
200
|
+
const completed = db.query(
|
|
201
|
+
`SELECT t.title, t.staff_role, t.completed_at, c.name as company_name,
|
|
202
|
+
s.role_name FROM opc_staff_tasks t
|
|
203
|
+
JOIN opc_companies c ON t.company_id = c.id
|
|
204
|
+
LEFT JOIN opc_staff_config s ON t.company_id = s.company_id AND t.staff_role = s.role
|
|
205
|
+
WHERE t.status = 'completed' AND t.completed_at > datetime('now', '-24 hours')
|
|
206
|
+
ORDER BY t.completed_at DESC LIMIT 5`,
|
|
207
|
+
) as { title: string; staff_role: string; completed_at: string; company_name: string; role_name: string }[];
|
|
208
|
+
|
|
209
|
+
if (completed.length > 0) {
|
|
210
|
+
lines.push("✅ **最近完成** (24h):");
|
|
211
|
+
for (const t of completed) {
|
|
212
|
+
lines.push(`- [${t.company_name}/${t.role_name ?? t.staff_role}] ${t.title}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (approvalTasks.length === 0 && inProgress.length === 0 && pending.length === 0 && completed.length === 0) {
|
|
217
|
+
lines.push("暂无任务。");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { text: lines.join("\n") };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── 今日简报 ────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
function renderBriefing(db: OpcDatabase): { text: string } {
|
|
226
|
+
const companies = db.query(
|
|
227
|
+
"SELECT id, name FROM opc_companies ORDER BY created_at DESC LIMIT 5",
|
|
228
|
+
) as { id: string; name: string }[];
|
|
229
|
+
|
|
230
|
+
if (companies.length === 0) {
|
|
231
|
+
return { text: "📰 **今日简报**\n\n暂无公司数据。" };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const lines: string[] = [`📰 **今日简报** (${new Date().toISOString().slice(0, 10)})`, ""];
|
|
235
|
+
|
|
236
|
+
for (const c of companies) {
|
|
237
|
+
const health = computeHealthScore(db, c.id);
|
|
238
|
+
const scorecard = computeGrowthScorecard(db, c.id);
|
|
239
|
+
const stageRow = db.queryOne(
|
|
240
|
+
"SELECT stage_label FROM opc_company_stage WHERE company_id = ?", c.id,
|
|
241
|
+
) as { stage_label: string } | null;
|
|
242
|
+
|
|
243
|
+
const income = (db.queryOne(
|
|
244
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'", c.id,
|
|
245
|
+
) as SumRow).total;
|
|
246
|
+
const expense = (db.queryOne(
|
|
247
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'expense'", c.id,
|
|
248
|
+
) as SumRow).total;
|
|
249
|
+
|
|
250
|
+
lines.push(`### ${c.name}`);
|
|
251
|
+
lines.push(`${stageRow?.stage_label ?? "未检测"} | 健康 ${health.total}分 | 评级 ${scorecard.overall}`);
|
|
252
|
+
lines.push(`收入: ${income.toLocaleString()} 元 | 支出: ${expense.toLocaleString()} 元 | 净利润: ${(income - expense).toLocaleString()} 元`);
|
|
253
|
+
|
|
254
|
+
// 告警
|
|
255
|
+
const alerts = db.query(
|
|
256
|
+
"SELECT title, severity FROM opc_alerts WHERE company_id = ? AND status = 'active' ORDER BY CASE severity WHEN 'critical' THEN 1 WHEN 'warning' THEN 2 ELSE 3 END LIMIT 2",
|
|
257
|
+
c.id,
|
|
258
|
+
) as { title: string; severity: string }[];
|
|
259
|
+
if (alerts.length > 0) {
|
|
260
|
+
lines.push(`告警: ${alerts.map(a => a.title).join(", ")}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 洞察
|
|
264
|
+
const insights = db.query(
|
|
265
|
+
`SELECT title FROM opc_insights WHERE company_id = ? AND status = 'active'
|
|
266
|
+
AND (expires_at = '' OR expires_at > datetime('now'))
|
|
267
|
+
ORDER BY priority DESC LIMIT 2`,
|
|
268
|
+
c.id,
|
|
269
|
+
) as { title: string }[];
|
|
270
|
+
if (insights.length > 0) {
|
|
271
|
+
lines.push(`洞察: ${insights.map(i => i.title).join(", ")}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
lines.push("");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { text: lines.join("\n") };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── 员工状态 ────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
function renderStaffStatus(db: OpcDatabase): { text: string } {
|
|
283
|
+
const lines: string[] = ["👥 **员工状态**", ""];
|
|
284
|
+
|
|
285
|
+
const companies = db.query(
|
|
286
|
+
"SELECT id, name FROM opc_companies ORDER BY created_at DESC LIMIT 5",
|
|
287
|
+
) as { id: string; name: string }[];
|
|
288
|
+
|
|
289
|
+
for (const c of companies) {
|
|
290
|
+
const staffList = db.query(
|
|
291
|
+
"SELECT role, role_name, enabled FROM opc_staff_config WHERE company_id = ? ORDER BY created_at ASC",
|
|
292
|
+
c.id,
|
|
293
|
+
) as { role: string; role_name: string; enabled: number }[];
|
|
294
|
+
|
|
295
|
+
if (staffList.length === 0) continue;
|
|
296
|
+
|
|
297
|
+
lines.push(`### ${c.name}`);
|
|
298
|
+
|
|
299
|
+
for (const s of staffList) {
|
|
300
|
+
if (!s.enabled) {
|
|
301
|
+
lines.push(`- ⏸️ ${s.role_name} (${s.role}) — 已停用`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const activeTasks = (db.queryOne(
|
|
306
|
+
"SELECT COUNT(*) as cnt FROM opc_staff_tasks WHERE company_id = ? AND staff_role = ? AND status IN ('in_progress', 'pending')",
|
|
307
|
+
c.id, s.role,
|
|
308
|
+
) as CountRow).cnt;
|
|
309
|
+
|
|
310
|
+
const status = activeTasks > 0 ? `🔄 ${activeTasks} 个任务` : "💤 空闲";
|
|
311
|
+
lines.push(`- ${s.role_name} (${s.role}) — ${status}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
lines.push("");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (companies.length === 0) {
|
|
318
|
+
lines.push("暂无公司数据。");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return { text: lines.join("\n") };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── 活跃告警 ────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
function renderAlerts(db: OpcDatabase): { text: string } {
|
|
327
|
+
const alerts = db.query(
|
|
328
|
+
`SELECT a.id, a.title, a.message, a.severity, a.category, a.created_at, c.name as company_name
|
|
329
|
+
FROM opc_alerts a
|
|
330
|
+
JOIN opc_companies c ON a.company_id = c.id
|
|
331
|
+
WHERE a.status = 'active'
|
|
332
|
+
ORDER BY CASE a.severity WHEN 'critical' THEN 1 WHEN 'warning' THEN 2 ELSE 3 END, a.created_at DESC
|
|
333
|
+
LIMIT 20`,
|
|
334
|
+
) as { id: string; title: string; message: string; severity: string; category: string; created_at: string; company_name: string }[];
|
|
335
|
+
|
|
336
|
+
if (alerts.length === 0) {
|
|
337
|
+
return { text: "🔔 **告警列表**\n\n无活跃告警。" };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const lines: string[] = [`🔔 **告警列表** (${alerts.length} 条)`, ""];
|
|
341
|
+
|
|
342
|
+
for (const a of alerts) {
|
|
343
|
+
const icon = a.severity === "critical" ? "🔴" : a.severity === "warning" ? "🟡" : "🔵";
|
|
344
|
+
lines.push(`${icon} [${a.company_name}] **${a.title}**`);
|
|
345
|
+
if (a.message) lines.push(` ${a.message}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return { text: lines.join("\n") };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── 公司详情 ────────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
function renderCompanyDetail(db: OpcDatabase, companyId: string): { text: string } {
|
|
354
|
+
// 尝试按 ID 查找,如果找不到尝试按名称模糊匹配
|
|
355
|
+
let company = db.queryOne(
|
|
356
|
+
"SELECT * FROM opc_companies WHERE id = ?", companyId,
|
|
357
|
+
) as Record<string, unknown> | null;
|
|
358
|
+
|
|
359
|
+
if (!company) {
|
|
360
|
+
company = db.queryOne(
|
|
361
|
+
"SELECT * FROM opc_companies WHERE name LIKE ?", `%${companyId}%`,
|
|
362
|
+
) as Record<string, unknown> | null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!company) {
|
|
366
|
+
return { text: `公司 "${companyId}" 不存在。` };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const cid = company.id as string;
|
|
370
|
+
const health = computeHealthScore(db, cid);
|
|
371
|
+
const scorecard = computeGrowthScorecard(db, cid);
|
|
372
|
+
const stageRow = db.queryOne(
|
|
373
|
+
"SELECT stage_label FROM opc_company_stage WHERE company_id = ?", cid,
|
|
374
|
+
) as { stage_label: string } | null;
|
|
375
|
+
|
|
376
|
+
const income = (db.queryOne(
|
|
377
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'income'", cid,
|
|
378
|
+
) as SumRow).total;
|
|
379
|
+
const expense = (db.queryOne(
|
|
380
|
+
"SELECT COALESCE(SUM(amount), 0) as total FROM opc_transactions WHERE company_id = ? AND type = 'expense'", cid,
|
|
381
|
+
) as SumRow).total;
|
|
382
|
+
|
|
383
|
+
const contactCount = (db.queryOne(
|
|
384
|
+
"SELECT COUNT(*) as cnt FROM opc_contacts WHERE company_id = ?", cid,
|
|
385
|
+
) as CountRow).cnt;
|
|
386
|
+
const contractCount = (db.queryOne(
|
|
387
|
+
"SELECT COUNT(*) as cnt FROM opc_contracts WHERE company_id = ? AND status = 'active'", cid,
|
|
388
|
+
) as CountRow).cnt;
|
|
389
|
+
const projectCount = (db.queryOne(
|
|
390
|
+
"SELECT COUNT(*) as cnt FROM opc_projects WHERE company_id = ? AND status IN ('active','planning')", cid,
|
|
391
|
+
) as CountRow).cnt;
|
|
392
|
+
const alertCount = (db.queryOne(
|
|
393
|
+
"SELECT COUNT(*) as cnt FROM opc_alerts WHERE company_id = ? AND status = 'active'", cid,
|
|
394
|
+
) as CountRow).cnt;
|
|
395
|
+
const taskCount = (db.queryOne(
|
|
396
|
+
"SELECT COUNT(*) as cnt FROM opc_staff_tasks WHERE company_id = ? AND status IN ('pending','in_progress','pending_approval')", cid,
|
|
397
|
+
) as CountRow).cnt;
|
|
398
|
+
|
|
399
|
+
const lines: string[] = [
|
|
400
|
+
`🏢 **${company.name}**`,
|
|
401
|
+
"",
|
|
402
|
+
`- 行业: ${company.industry} | 状态: ${company.status}`,
|
|
403
|
+
`- 阶段: ${stageRow?.stage_label ?? "未检测"} | 健康: ${health.total}分 | 评级: ${scorecard.overall}`,
|
|
404
|
+
`- 创办人: ${company.owner_name} | 注册资本: ${(company.registered_capital as number).toLocaleString()} 元`,
|
|
405
|
+
"",
|
|
406
|
+
"**财务**",
|
|
407
|
+
`- 总收入: ${income.toLocaleString()} 元 | 总支出: ${expense.toLocaleString()} 元 | 净利润: ${(income - expense).toLocaleString()} 元`,
|
|
408
|
+
"",
|
|
409
|
+
"**运营**",
|
|
410
|
+
`- 客户: ${contactCount} | 活跃合同: ${contractCount} | 活跃项目: ${projectCount}`,
|
|
411
|
+
`- 活跃告警: ${alertCount} | 待办任务: ${taskCount}`,
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
// 评分卡详情
|
|
415
|
+
lines.push("");
|
|
416
|
+
lines.push("**评分卡**");
|
|
417
|
+
for (const d of scorecard.dimensions) {
|
|
418
|
+
lines.push(`- ${d.name}: ${d.grade} — ${d.detail}`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return { text: lines.join("\n") };
|
|
422
|
+
}
|
package/src/db/index.ts
CHANGED