galaxy-opc-plugin 0.2.1 → 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.
Files changed (41) hide show
  1. package/index.ts +244 -8
  2. package/package.json +17 -3
  3. package/src/__tests__/e2e/company-lifecycle.test.ts +399 -0
  4. package/src/__tests__/integration/business-workflows.test.ts +366 -0
  5. package/src/__tests__/test-utils.ts +316 -0
  6. package/src/commands/opc-command.ts +422 -0
  7. package/src/db/index.ts +3 -0
  8. package/src/db/migrations.test.ts +324 -0
  9. package/src/db/migrations.ts +131 -0
  10. package/src/db/schema.ts +211 -0
  11. package/src/db/sqlite-adapter.ts +5 -0
  12. package/src/opc/autonomy-rules.ts +132 -0
  13. package/src/opc/briefing-builder.ts +1331 -0
  14. package/src/opc/business-workflows.test.ts +535 -0
  15. package/src/opc/business-workflows.ts +325 -0
  16. package/src/opc/context-injector.ts +366 -28
  17. package/src/opc/event-triggers.ts +472 -0
  18. package/src/opc/intelligence-engine.ts +702 -0
  19. package/src/opc/milestone-detector.ts +251 -0
  20. package/src/opc/proactive-service.ts +179 -0
  21. package/src/opc/reminder-service.ts +4 -43
  22. package/src/opc/session-task-tracker.ts +60 -0
  23. package/src/opc/stage-detector.ts +168 -0
  24. package/src/opc/task-executor.ts +332 -0
  25. package/src/opc/task-templates.ts +179 -0
  26. package/src/tools/document-tool.ts +1176 -0
  27. package/src/tools/finance-tool.test.ts +238 -0
  28. package/src/tools/finance-tool.ts +922 -14
  29. package/src/tools/hr-tool.ts +10 -1
  30. package/src/tools/legal-tool.test.ts +251 -0
  31. package/src/tools/legal-tool.ts +26 -4
  32. package/src/tools/lifecycle-tool.test.ts +231 -0
  33. package/src/tools/media-tool.ts +156 -1
  34. package/src/tools/monitoring-tool.ts +134 -1
  35. package/src/tools/opc-tool.test.ts +250 -0
  36. package/src/tools/opc-tool.ts +251 -28
  37. package/src/tools/project-tool.test.ts +218 -0
  38. package/src/tools/schemas.ts +80 -0
  39. package/src/tools/search-tool.ts +227 -0
  40. package/src/tools/staff-tool.ts +395 -2
  41. package/src/web/config-ui.ts +299 -45
package/index.ts CHANGED
@@ -14,7 +14,16 @@ import { registerHttpRoutes } from "./src/api/routes.js";
14
14
  import type { OpcDatabase } from "./src/db/index.js";
15
15
  import { SqliteAdapter } from "./src/db/sqlite-adapter.js";
16
16
  import { registerContextInjector } from "./src/opc/context-injector.js";
17
- import { startReminderService } from "./src/opc/reminder-service.js";
17
+ import { startProactiveService } from "./src/opc/proactive-service.js";
18
+ import { runIntelligenceScanForCompany } from "./src/opc/intelligence-engine.js";
19
+ import { detectMilestones } from "./src/opc/milestone-detector.js";
20
+ import { updateCompanyStage } from "./src/opc/stage-detector.js";
21
+ import {
22
+ registerSpawnedSession,
23
+ getSessionTaskMapping,
24
+ removeSessionTaskMapping,
25
+ clearAllSessionMappings,
26
+ } from "./src/opc/session-task-tracker.js";
18
27
  import { registerAcquisitionTool } from "./src/tools/acquisition-tool.js";
19
28
  import { registerAssetPackageTool } from "./src/tools/asset-package-tool.js";
20
29
  import { registerFinanceTool } from "./src/tools/finance-tool.js";
@@ -28,7 +37,11 @@ import { registerOpcTool } from "./src/tools/opc-tool.js";
28
37
  import { registerOpbTool } from "./src/tools/opb-tool.js";
29
38
  import { registerProcurementTool } from "./src/tools/procurement-tool.js";
30
39
  import { registerProjectTool } from "./src/tools/project-tool.js";
40
+ import { registerSearchTool } from "./src/tools/search-tool.js";
31
41
  import { registerStaffTool } from "./src/tools/staff-tool.js";
42
+ import { registerDocumentTool } from "./src/tools/document-tool.js";
43
+ import { registerOpcCommand } from "./src/commands/opc-command.js";
44
+ import { triggerEventRules } from "./src/opc/event-triggers.js";
32
45
  import { registerConfigUi } from "./src/web/config-ui.js";
33
46
  import { registerLandingPage } from "./src/web/landing-page.js";
34
47
 
@@ -42,6 +55,12 @@ function resolveDbPath(configured?: string): string {
42
55
  return dbPath;
43
56
  }
44
57
 
58
+ /** 从 sessionKey 中提取公司 ID(格式: agent:opc-{companyId}:subagent:...) */
59
+ function extractCompanyIdFromSession(sessionKey: string): string | null {
60
+ const match = sessionKey.match(/agent:opc-([^:]+):/);
61
+ return match ? match[1] : null;
62
+ }
63
+
45
64
  let db: OpcDatabase | null = null;
46
65
 
47
66
  const plugin = {
@@ -78,6 +97,7 @@ const plugin = {
78
97
  registerOpcTool(api, db);
79
98
  registerStaffTool(api, db);
80
99
  registerOpbTool(api, db);
100
+ registerSearchTool(api);
81
101
 
82
102
  // 注册 Phase 2 专业工具(可通过管理后台禁用)
83
103
  if (isEnabled("opc_finance")) registerFinanceTool(api, db);
@@ -92,6 +112,9 @@ const plugin = {
92
112
  if (isEnabled("opc_lifecycle")) registerLifecycleTool(api, db);
93
113
  if (isEnabled("opc_monitoring")) registerMonitoringTool(api, db);
94
114
 
115
+ // 文档生成工具(始终启用)
116
+ registerDocumentTool(api, db);
117
+
95
118
  // 资金闭环工具(始终启用,核心商业模式)
96
119
  registerAcquisitionTool(api, db);
97
120
  registerAssetPackageTool(api, db);
@@ -101,6 +124,9 @@ const plugin = {
101
124
  // 注册上下文注入钩子
102
125
  registerContextInjector(api, db);
103
126
 
127
+ // 注册 /opc 快捷命令(毫秒级仪表盘,不经 LLM)
128
+ registerOpcCommand(api, db);
129
+
104
130
  // 读取 gateway token 用于 API 认证
105
131
  const gatewayToken = (() => {
106
132
  try {
@@ -118,8 +144,213 @@ const plugin = {
118
144
  registerConfigUi(api, db, gatewayToken);
119
145
  registerLandingPage(api);
120
146
 
121
- // 注册后台服务(数据库生命周期 + 自动提醒)
122
- let stopReminder: (() => void) | null = null;
147
+ // ── 智能刷新器(共享函数,after_tool_call + subagent_ended 共用) ──
148
+ const refreshTimers = new Map<string, ReturnType<typeof setTimeout>>();
149
+ function triggerIntelligenceRefresh(companyId: string): void {
150
+ const existing = refreshTimers.get(companyId);
151
+ if (existing) clearTimeout(existing);
152
+ refreshTimers.set(companyId, setTimeout(() => {
153
+ refreshTimers.delete(companyId);
154
+ const l = (msg: string) => api.logger.info(msg);
155
+ db!.execute(
156
+ "DELETE FROM opc_insights WHERE company_id = ? AND insight_type = 'data_gap'",
157
+ companyId,
158
+ );
159
+ updateCompanyStage(db!, companyId);
160
+ runIntelligenceScanForCompany(db!, companyId, l);
161
+ detectMilestones(db!, companyId, l);
162
+ }, 5000));
163
+ }
164
+
165
+ // 注册 after_tool_call 钩子 — 工具调用后即时刷新洞察(5秒防抖)+ 拦截 sessions_spawn
166
+ api.on("after_tool_call", (event, ctx) => {
167
+ const aid = ctx.agentId;
168
+ if (!aid?.startsWith("opc-")) return;
169
+ const companyId = aid.slice(4);
170
+
171
+ // ── 拦截 sessions_spawn:提取 taskId + childSessionKey 建立映射 ──
172
+ const toolName = String((event as Record<string, unknown>).toolName ?? "");
173
+ if (toolName === "sessions_spawn") {
174
+ try {
175
+ const taskParam = (event.params as Record<string, unknown>)?.task as string;
176
+ const match = taskParam?.match(/## 任务 ID\n([a-z0-9-]+)/);
177
+ if (match) {
178
+ const taskId = match[1];
179
+ const result = event.result as Record<string, unknown> | undefined;
180
+ const childSessionKey = (result?.childSessionKey || result?.sessionKey) as string | undefined;
181
+ if (childSessionKey) {
182
+ // 从任务记录获取 staffRole 和 title
183
+ const taskRow = db!.queryOne(
184
+ "SELECT staff_role, title FROM opc_staff_tasks WHERE id = ?", taskId,
185
+ ) as { staff_role: string; title: string } | null;
186
+
187
+ registerSpawnedSession(childSessionKey, {
188
+ taskId,
189
+ companyId,
190
+ staffRole: taskRow?.staff_role ?? "",
191
+ title: taskRow?.title ?? "",
192
+ runId: result?.runId as string | undefined,
193
+ spawnedAt: new Date().toISOString(),
194
+ });
195
+
196
+ // 持久化 session_key 到数据库
197
+ db!.execute(
198
+ "UPDATE opc_staff_tasks SET session_key = ? WHERE id = ?",
199
+ childSessionKey, taskId,
200
+ );
201
+
202
+ api.logger.info(
203
+ `opc: 已追踪子会话 ${childSessionKey} → 任务 ${taskId} (${taskRow?.title ?? "?"})`,
204
+ );
205
+ }
206
+ }
207
+ } catch (err) {
208
+ api.logger.info(`opc: sessions_spawn 追踪失败: ${err instanceof Error ? err.message : String(err)}`);
209
+ }
210
+ }
211
+
212
+ // ── 事件驱动触发引擎:OPC 工具写入数据后自动检查业务规则 ──
213
+ if (toolName.startsWith("opc_")) {
214
+ triggerEventRules(
215
+ db!, companyId, toolName,
216
+ event.params as Record<string, unknown> | undefined,
217
+ event.result as Record<string, unknown> | undefined,
218
+ (msg) => api.logger.info(msg),
219
+ );
220
+ }
221
+
222
+ // ── 通用:刷新洞察 ──
223
+ triggerIntelligenceRefresh(companyId);
224
+ });
225
+
226
+ // ── subagent_ended 钩子 — 子会话结束时自动更新任务状态 ──
227
+ api.on("subagent_ended", (event) => {
228
+ try {
229
+ const ev = event as Record<string, unknown>;
230
+ // 优先使用 targetSessionKey(SDK 标准字段),fallback 到 context 中的 childSessionKey
231
+ const sessionKey = ev.targetSessionKey as string
232
+ ?? (ev as Record<string, unknown>).childSessionKey as string
233
+ ?? ev.sessionKey as string;
234
+ if (!sessionKey) return;
235
+
236
+ const mapping = getSessionTaskMapping(sessionKey);
237
+ if (!mapping) {
238
+ // 非 OPC 任务,也尝试从数据库查找(服务重启后内存映射丢失的情况)
239
+ const dbTask = db!.queryOne(
240
+ "SELECT id, company_id, staff_role, title, status FROM opc_staff_tasks WHERE session_key = ?",
241
+ sessionKey,
242
+ ) as { id: string; company_id: string; staff_role: string; title: string; status: string } | null;
243
+ if (!dbTask) return;
244
+
245
+ // 从数据库恢复映射并处理
246
+ handleSubagentEnd(dbTask.id, dbTask.company_id, dbTask.status, event);
247
+ return;
248
+ }
249
+
250
+ const task = db!.queryOne(
251
+ "SELECT status FROM opc_staff_tasks WHERE id = ?", mapping.taskId,
252
+ ) as { status: string } | null;
253
+ if (!task) {
254
+ removeSessionTaskMapping(sessionKey);
255
+ return;
256
+ }
257
+
258
+ handleSubagentEnd(mapping.taskId, mapping.companyId, task.status, event);
259
+ removeSessionTaskMapping(sessionKey);
260
+ } catch (err) {
261
+ api.logger.info(`opc: subagent_ended 处理失败: ${err instanceof Error ? err.message : String(err)}`);
262
+ }
263
+ });
264
+
265
+ function handleSubagentEnd(
266
+ taskId: string,
267
+ companyId: string,
268
+ currentStatus: string,
269
+ event: Record<string, unknown>,
270
+ ): void {
271
+ const now = new Date().toISOString();
272
+ const outcome = event.outcome as string | undefined;
273
+
274
+ if (outcome === "ok" || outcome === "completed") {
275
+ // 员工正常结束 — 如果任务仍 in_progress,说明员工忘了调 update_task
276
+ if (currentStatus === "in_progress") {
277
+ db!.execute(
278
+ `UPDATE opc_staff_tasks SET status = 'completed', completed_at = ?,
279
+ result_summary = CASE WHEN result_summary = '' THEN ? ELSE result_summary END
280
+ WHERE id = ? AND status = 'in_progress'`,
281
+ now,
282
+ "[系统] 员工会话已正常结束但未提交工作报告",
283
+ taskId,
284
+ );
285
+ api.logger.info(`opc: 子会话正常结束,自动标记任务 ${taskId} 为 completed`);
286
+ }
287
+ } else {
288
+ // error/timeout/killed → 自动取消
289
+ const errorDetail = event.error as string || "";
290
+ db!.execute(
291
+ `UPDATE opc_staff_tasks SET status = 'cancelled', completed_at = ?,
292
+ result_summary = ? WHERE id = ? AND status IN ('in_progress', 'pending')`,
293
+ now,
294
+ `[系统] 员工会话异常终止(${outcome || "unknown"}: ${errorDetail})`,
295
+ taskId,
296
+ );
297
+ api.logger.info(`opc: 子会话异常终止(${outcome}),自动取消任务 ${taskId}`);
298
+ }
299
+
300
+ triggerIntelligenceRefresh(companyId);
301
+ }
302
+
303
+ // ── before_tool_call 权限控制 + switch_company 注入 ──
304
+ api.on("before_tool_call", (event, ctx) => {
305
+ // ── switch_company 自动注入 channel/peer 信息 ──
306
+ const btToolName0 = String((event as Record<string, unknown>).toolName ?? "");
307
+ if (btToolName0 === "opc_manage") {
308
+ const action = (event.params as Record<string, unknown>)?.action;
309
+ if (action === "switch_company" && ctx.sessionKey) {
310
+ const sessionKey = ctx.sessionKey;
311
+ api.logger.info(`opc: switch_company sessionKey = "${sessionKey}"`);
312
+ api.logger.info(`opc: switch_company agentId = "${ctx.agentId}", params = ${JSON.stringify(event.params)}`);
313
+ // 提取 channel: ":direct:" 前面的标识(如 feishu)
314
+ const channelMatch = sessionKey.match(/:([a-z_]+):direct:/);
315
+ const peerMatch = sessionKey.match(/:direct:([^:]+)/);
316
+ const channel = channelMatch?.[1] ?? "";
317
+ const peerId = peerMatch?.[1] ?? "";
318
+ api.logger.info(`opc: switch_company parsed channel="${channel}", peerId="${peerId}"`);
319
+ if (channel && peerId) {
320
+ return {
321
+ params: {
322
+ ...(event.params as Record<string, unknown> || {}),
323
+ _channel: channel,
324
+ _peer_id: peerId,
325
+ },
326
+ };
327
+ }
328
+ }
329
+ }
330
+
331
+ // ── 子会话权限控制 ──
332
+ // 非子会话不拦截
333
+ if (!ctx.sessionKey?.includes("subagent")) return;
334
+
335
+ const btToolName = String((event as Record<string, unknown>).toolName ?? "");
336
+ if (!btToolName.startsWith("opc_")) return; // 非 OPC 工具不拦截
337
+
338
+ // 从 params 中提取 company_id
339
+ const paramCompanyId = (event.params as Record<string, unknown>)?.company_id as string | undefined;
340
+ if (!paramCompanyId) return;
341
+
342
+ // 从父会话 agentId 或 sessionKey 提取公司 ID
343
+ const parentCompanyId = extractCompanyIdFromSession(ctx.sessionKey);
344
+ if (parentCompanyId && paramCompanyId !== parentCompanyId) {
345
+ return {
346
+ block: true,
347
+ blockReason: `权限拒绝:你只能操作公司 ${parentCompanyId} 的数据,不能操作 ${paramCompanyId}`,
348
+ };
349
+ }
350
+ });
351
+
352
+ // 注册后台服务(数据库生命周期 + 主动智能)
353
+ let stopProactive: (() => void) | null = null;
123
354
  api.registerService({
124
355
  id: "opc-db-lifecycle",
125
356
  start() {
@@ -129,17 +360,22 @@ const plugin = {
129
360
  "SELECT value FROM opc_tool_config WHERE key = ?", "webhook_url",
130
361
  ) as { value: string } | null;
131
362
  const webhookUrl = webhookRow?.value?.trim() || undefined;
132
- // 启动自动提醒服务(每小时扫描一次)
133
- stopReminder = startReminderService(
363
+ // 启动主动智能服务(每小时全量扫描)
364
+ stopProactive = startProactiveService(
134
365
  db!,
135
366
  (msg) => api.logger.info(msg),
136
367
  webhookUrl,
137
368
  );
138
- api.logger.info(`opc: 自动提醒服务已启动(每小时扫描${webhookUrl ? ",Webhook 已配置" : ""})`);
369
+ api.logger.info(`opc: 主动智能服务已启动(每小时扫描${webhookUrl ? ",Webhook 已配置" : ""})`);
139
370
  },
140
371
  stop() {
141
- stopReminder?.();
142
- stopReminder = null;
372
+ stopProactive?.();
373
+ stopProactive = null;
374
+ // 清理防抖定时器
375
+ for (const timer of refreshTimers.values()) clearTimeout(timer);
376
+ refreshTimers.clear();
377
+ // 清理会话映射
378
+ clearAllSessionMappings();
143
379
  if (db) {
144
380
  db.close();
145
381
  db = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galaxy-opc-plugin",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "星环 Galaxy OPC — 一人公司孵化与赋能平台 OpenClaw 插件",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -30,10 +30,24 @@
30
30
  ],
31
31
  "dependencies": {
32
32
  "@sinclair/typebox": "0.34.48",
33
- "better-sqlite3": "^11.8.1"
33
+ "@types/pdfkit": "^0.17.5",
34
+ "better-sqlite3": "^11.8.1",
35
+ "docx": "^9.6.0",
36
+ "exceljs": "^4.4.0",
37
+ "pdfkit": "^0.17.2"
34
38
  },
35
39
  "devDependencies": {
36
- "@types/better-sqlite3": "^7.6.12"
40
+ "@types/better-sqlite3": "^7.6.12",
41
+ "@types/node": "^20.19.35",
42
+ "@vitest/coverage-v8": "^2.1.9",
43
+ "@vitest/ui": "^2.1.9",
44
+ "vitest": "^2.1.9"
45
+ },
46
+ "scripts": {
47
+ "test": "vitest",
48
+ "test:ui": "vitest --ui",
49
+ "test:run": "vitest run",
50
+ "test:coverage": "vitest run --coverage"
37
51
  },
38
52
  "openclaw": {
39
53
  "extensions": [