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.
- package/index.ts +244 -8
- package/package.json +17 -3
- package/src/__tests__/e2e/company-lifecycle.test.ts +399 -0
- package/src/__tests__/integration/business-workflows.test.ts +366 -0
- package/src/__tests__/test-utils.ts +316 -0
- package/src/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/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 +134 -1
- 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
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 {
|
|
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
|
-
|
|
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
|
-
|
|
363
|
+
// 启动主动智能服务(每小时全量扫描)
|
|
364
|
+
stopProactive = startProactiveService(
|
|
134
365
|
db!,
|
|
135
366
|
(msg) => api.logger.info(msg),
|
|
136
367
|
webhookUrl,
|
|
137
368
|
);
|
|
138
|
-
api.logger.info(`opc:
|
|
369
|
+
api.logger.info(`opc: 主动智能服务已启动(每小时扫描${webhookUrl ? ",Webhook 已配置" : ""})`);
|
|
139
370
|
},
|
|
140
371
|
stop() {
|
|
141
|
-
|
|
142
|
-
|
|
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.
|
|
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
|
-
"
|
|
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": [
|