galaxy-opc-plugin 0.1.0
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/README.md +166 -0
- package/index.ts +145 -0
- package/openclaw.plugin.json +17 -0
- package/package.json +47 -0
- package/skills/basic-crm/SKILL.md +81 -0
- package/skills/basic-finance/SKILL.md +100 -0
- package/skills/business-monitoring/SKILL.md +120 -0
- package/skills/company-lifecycle/SKILL.md +99 -0
- package/skills/company-registration/SKILL.md +80 -0
- package/skills/finance-tax/SKILL.md +150 -0
- package/skills/hr-assistant/SKILL.md +127 -0
- package/skills/investment-management/SKILL.md +101 -0
- package/skills/legal-assistant/SKILL.md +113 -0
- package/skills/media-ops/SKILL.md +101 -0
- package/skills/procurement-management/SKILL.md +91 -0
- package/skills/project-management/SKILL.md +125 -0
- package/src/api/companies.ts +193 -0
- package/src/api/dashboard.ts +25 -0
- package/src/api/routes.ts +14 -0
- package/src/db/index.ts +63 -0
- package/src/db/migrations.ts +67 -0
- package/src/db/schema.ts +518 -0
- package/src/db/sqlite-adapter.ts +366 -0
- package/src/opc/company-manager.ts +82 -0
- package/src/opc/context-injector.ts +186 -0
- package/src/opc/reminder-service.ts +289 -0
- package/src/opc/types.ts +330 -0
- package/src/opc/workspace-factory.ts +189 -0
- package/src/tools/acquisition-tool.ts +150 -0
- package/src/tools/asset-package-tool.ts +283 -0
- package/src/tools/finance-tool.ts +244 -0
- package/src/tools/hr-tool.ts +211 -0
- package/src/tools/investment-tool.ts +201 -0
- package/src/tools/legal-tool.ts +191 -0
- package/src/tools/lifecycle-tool.ts +251 -0
- package/src/tools/media-tool.ts +174 -0
- package/src/tools/monitoring-tool.ts +207 -0
- package/src/tools/opb-tool.ts +193 -0
- package/src/tools/opc-tool.ts +206 -0
- package/src/tools/procurement-tool.ts +191 -0
- package/src/tools/project-tool.ts +203 -0
- package/src/tools/schemas.ts +163 -0
- package/src/tools/staff-tool.ts +211 -0
- package/src/utils/tool-helper.ts +16 -0
- package/src/web/config-ui.ts +3501 -0
- package/src/web/landing-page.ts +269 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — Agent 工作区工厂
|
|
3
|
+
*
|
|
4
|
+
* 为每家一人公司创建独立的 Agent 工作区。
|
|
5
|
+
* 参照 feishu/dynamic-agent.ts 模式。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
12
|
+
|
|
13
|
+
export type CreateWorkspaceResult = {
|
|
14
|
+
created: boolean;
|
|
15
|
+
agentId: string;
|
|
16
|
+
updatedCfg?: OpenClawConfig;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 为公司创建或复用 Agent 工作区。
|
|
21
|
+
* Agent ID 格式: opc-{companyId}
|
|
22
|
+
*/
|
|
23
|
+
export async function ensureCompanyWorkspace(params: {
|
|
24
|
+
companyId: string;
|
|
25
|
+
companyName: string;
|
|
26
|
+
cfg: OpenClawConfig;
|
|
27
|
+
runtime: PluginRuntime;
|
|
28
|
+
log: (msg: string) => void;
|
|
29
|
+
skills?: string[];
|
|
30
|
+
}): Promise<CreateWorkspaceResult> {
|
|
31
|
+
const { companyId, companyName, runtime, log, skills } = params;
|
|
32
|
+
const agentId = `opc-${companyId}`;
|
|
33
|
+
|
|
34
|
+
// 每次从磁盘读取最新配置,避免用插件启动时的快照覆盖后续写入
|
|
35
|
+
const latestCfg = await runtime.config.loadConfig();
|
|
36
|
+
|
|
37
|
+
// 检查 Agent 是否已存在
|
|
38
|
+
const existingAgent = (latestCfg.agents?.list ?? []).find((a) => a.id === agentId);
|
|
39
|
+
if (existingAgent) {
|
|
40
|
+
log(`opc: Agent "${agentId}" 已存在,跳过创建`);
|
|
41
|
+
return { created: false, agentId };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 从主 OPC agent 继承 model 配置;若找不到则用 defaults
|
|
45
|
+
const opcAgent = (latestCfg.agents?.list ?? []).find((a) => a.id === "opc");
|
|
46
|
+
const inheritedModel = (opcAgent as { model?: unknown } | undefined)?.model
|
|
47
|
+
?? latestCfg.agents?.defaults?.model;
|
|
48
|
+
|
|
49
|
+
// 解析工作区路径
|
|
50
|
+
const workspace = resolveUserPath(`~/.openclaw/opc-workspaces/${companyId}`);
|
|
51
|
+
const agentDir = resolveUserPath(`~/.openclaw/opc-workspaces/${companyId}/agent`);
|
|
52
|
+
|
|
53
|
+
log(`opc: 创建 Agent 工作区 "${agentId}" (${companyName})`);
|
|
54
|
+
log(` workspace: ${workspace}`);
|
|
55
|
+
log(` agentDir: ${agentDir}`);
|
|
56
|
+
|
|
57
|
+
// 创建目录
|
|
58
|
+
await fs.promises.mkdir(workspace, { recursive: true });
|
|
59
|
+
await fs.promises.mkdir(agentDir, { recursive: true });
|
|
60
|
+
|
|
61
|
+
// 写入 AGENTS.md,内含该公司专属的 sessions_send 回传 session key
|
|
62
|
+
const agentsMdPath = path.join(workspace, "AGENTS.md");
|
|
63
|
+
const agentsMdExists = await fs.promises.access(agentsMdPath).then(() => true).catch(() => false);
|
|
64
|
+
if (!agentsMdExists) {
|
|
65
|
+
const sessionKey = `agent:${agentId}:main`;
|
|
66
|
+
await fs.promises.writeFile(agentsMdPath, buildOpcAgentsMd(companyName, sessionKey), "utf-8");
|
|
67
|
+
log(`opc: 已写入 AGENTS.md (sessionKey: ${sessionKey})`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 构造新 Agent 条目(类型从 OpenClawConfig 推断,避免依赖未导出的 AgentConfig)
|
|
71
|
+
type AgentEntry = NonNullable<NonNullable<OpenClawConfig["agents"]>["list"]>[number];
|
|
72
|
+
const newAgent: AgentEntry = {
|
|
73
|
+
id: agentId,
|
|
74
|
+
name: companyName,
|
|
75
|
+
workspace,
|
|
76
|
+
agentDir,
|
|
77
|
+
identity: {
|
|
78
|
+
name: companyName,
|
|
79
|
+
theme: "一人公司 AI 员工,提供行政、财务、HR、法务全方位支持",
|
|
80
|
+
emoji: "🏢",
|
|
81
|
+
},
|
|
82
|
+
subagents: { allowAgents: ["*"] } as AgentEntry["subagents"],
|
|
83
|
+
...(inheritedModel ? { model: inheritedModel as AgentEntry["model"] } : {}),
|
|
84
|
+
...(skills && skills.length > 0 ? { skills: skills as AgentEntry["skills"] } : {}),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// 更新配置 — 新增 Agent(不添加 feishu binding,避免干扰现有渠道配置)
|
|
88
|
+
const updatedCfg: OpenClawConfig = {
|
|
89
|
+
...latestCfg,
|
|
90
|
+
agents: {
|
|
91
|
+
...latestCfg.agents,
|
|
92
|
+
list: [
|
|
93
|
+
...(latestCfg.agents?.list ?? []),
|
|
94
|
+
newAgent,
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
await runtime.config.writeConfigFile(updatedCfg);
|
|
100
|
+
log(`opc: Agent "${agentId}" 已写入配置文件,重启 Gateway 后生效`);
|
|
101
|
+
return { created: true, agentId, updatedCfg };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveUserPath(p: string): string {
|
|
105
|
+
if (p.startsWith("~/")) {
|
|
106
|
+
return path.join(os.homedir(), p.slice(2));
|
|
107
|
+
}
|
|
108
|
+
return p;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 生成公司专属的 AGENTS.md 内容。
|
|
113
|
+
* sessionKey 硬编码为该公司 agent 的 main session,确保 subagent 回传时不会路由到错误的公司。
|
|
114
|
+
*/
|
|
115
|
+
function buildOpcAgentsMd(companyName: string, sessionKey: string): string {
|
|
116
|
+
return `# AGENTS.md - Your Workspace
|
|
117
|
+
|
|
118
|
+
This folder is home. Treat it that way.
|
|
119
|
+
|
|
120
|
+
## Every Session
|
|
121
|
+
|
|
122
|
+
Before doing anything else:
|
|
123
|
+
|
|
124
|
+
1. Read \`SOUL.md\` — this is who you are
|
|
125
|
+
2. Read \`USER.md\` — this is who you're helping
|
|
126
|
+
3. Read \`memory/YYYY-MM-DD.md\` (today + yesterday) for recent context
|
|
127
|
+
4. **If in MAIN SESSION** (direct chat with your human): Also read \`MEMORY.md\`
|
|
128
|
+
|
|
129
|
+
Don't ask permission. Just do it.
|
|
130
|
+
|
|
131
|
+
## Memory
|
|
132
|
+
|
|
133
|
+
You wake up fresh each session. These files are your continuity:
|
|
134
|
+
|
|
135
|
+
- **Daily notes:** \`memory/YYYY-MM-DD.md\` (create \`memory/\` if needed) — raw logs of what happened
|
|
136
|
+
- **Long-term:** \`MEMORY.md\` — your curated memories, like a human's long-term memory
|
|
137
|
+
|
|
138
|
+
## Safety
|
|
139
|
+
|
|
140
|
+
- Don't exfiltrate private data. Ever.
|
|
141
|
+
- Don't run destructive commands without asking.
|
|
142
|
+
- When in doubt, ask.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## OPC ${companyName} — AI 员工调度规则
|
|
147
|
+
|
|
148
|
+
你是${companyName}的 AI 助理总管,服务于公司老板(用户)。
|
|
149
|
+
|
|
150
|
+
### 核心职责
|
|
151
|
+
|
|
152
|
+
- 接收老板指令,判断是自己处理还是派遣 AI 员工
|
|
153
|
+
- 通过 \`sessions_spawn\` 工具把专业任务分配给对应 AI 员工
|
|
154
|
+
- 多任务时并行派遣,不必串行等待
|
|
155
|
+
- 员工完成后结果自动回到本对话,你负责简洁汇总
|
|
156
|
+
|
|
157
|
+
### 派遣任务的格式
|
|
158
|
+
|
|
159
|
+
调用 \`sessions_spawn\` 时,\`task\` 参数内容:
|
|
160
|
+
|
|
161
|
+
\`\`\`
|
|
162
|
+
[角色设定]
|
|
163
|
+
{该员工的系统提示词,从上下文中的"AI 员工团队"部分获取}
|
|
164
|
+
|
|
165
|
+
[公司信息]
|
|
166
|
+
公司名称:${companyName}
|
|
167
|
+
{其他公司信息}
|
|
168
|
+
|
|
169
|
+
[任务]
|
|
170
|
+
{老板交代的具体任务,要详细完整,因为员工没有本对话的上下文}
|
|
171
|
+
|
|
172
|
+
[完成后操作]
|
|
173
|
+
任务完成后,必须使用 sessions_send 工具将结果发送回:
|
|
174
|
+
- sessionKey: "${sessionKey}"
|
|
175
|
+
- message: 你的完整工作结果报告
|
|
176
|
+
\`\`\`
|
|
177
|
+
|
|
178
|
+
### 调度判断
|
|
179
|
+
|
|
180
|
+
- 老板明确说「让财务/法务/HR...」→ 直接派对应员工
|
|
181
|
+
- 老板说「帮我算税/出报表」→ 判断属于哪个专业方向再派
|
|
182
|
+
- 多件事同时 → 并行派多个员工
|
|
183
|
+
- 闲聊/简单问题 → 自己直接回答
|
|
184
|
+
|
|
185
|
+
### 工具说明
|
|
186
|
+
|
|
187
|
+
AI 员工在 subagent 里运行,可以使用所有 opc_* 工具操作公司数据库,结果会持久化保存。
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — opc_acquisition 收并购管理工具
|
|
3
|
+
*
|
|
4
|
+
* 资金闭环核心模块:当一人公司经营不善时,星河数科依据参股协议
|
|
5
|
+
* 发起收并购,亏损可抵扣应纳税所得额,并为后续资产包打包做准备。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
9
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
10
|
+
import type { OpcDatabase } from "../db/index.js";
|
|
11
|
+
import { json } from "../utils/tool-helper.js";
|
|
12
|
+
|
|
13
|
+
const AcquisitionSchema = Type.Union([
|
|
14
|
+
Type.Object({
|
|
15
|
+
action: Type.Literal("create_acquisition"),
|
|
16
|
+
company_id: Type.String({ description: "被收购公司 ID" }),
|
|
17
|
+
trigger_reason: Type.String({ description: "发起收购原因,如:连续亏损、市场萎缩、创始人退出等" }),
|
|
18
|
+
acquisition_price: Type.Number({ description: "收购价格(元),通常低于注册资本" }),
|
|
19
|
+
loss_amount: Type.Optional(Type.Number({ description: "公司累计亏损金额(元),用于税务抵扣计算" })),
|
|
20
|
+
notes: Type.Optional(Type.String({ description: "备注" })),
|
|
21
|
+
}),
|
|
22
|
+
Type.Object({
|
|
23
|
+
action: Type.Literal("list_acquisitions"),
|
|
24
|
+
status: Type.Optional(Type.String({ description: "按状态筛选: evaluating/in_progress/completed/cancelled" })),
|
|
25
|
+
company_id: Type.Optional(Type.String({ description: "按公司筛选" })),
|
|
26
|
+
}),
|
|
27
|
+
Type.Object({
|
|
28
|
+
action: Type.Literal("update_acquisition"),
|
|
29
|
+
case_id: Type.String({ description: "收并购案例 ID" }),
|
|
30
|
+
status: Type.Optional(Type.String({ description: "新状态: evaluating/in_progress/completed/cancelled" })),
|
|
31
|
+
acquisition_price: Type.Optional(Type.Number({ description: "最终收购价格(元)" })),
|
|
32
|
+
loss_amount: Type.Optional(Type.Number({ description: "确认亏损金额(元)" })),
|
|
33
|
+
tax_deduction: Type.Optional(Type.Number({ description: "可抵扣税额(元),通常 = 亏损 × 企业所得税率25%" })),
|
|
34
|
+
closed_date: Type.Optional(Type.String({ description: "完成日期 (YYYY-MM-DD)" })),
|
|
35
|
+
notes: Type.Optional(Type.String({ description: "备注" })),
|
|
36
|
+
}),
|
|
37
|
+
Type.Object({
|
|
38
|
+
action: Type.Literal("acquisition_summary"),
|
|
39
|
+
description: Type.Optional(Type.String({ description: "汇总平台所有收并购数据,含税务优化总额" })),
|
|
40
|
+
}),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
type AcquisitionParams = Static<typeof AcquisitionSchema>;
|
|
44
|
+
|
|
45
|
+
export function registerAcquisitionTool(api: OpenClawPluginApi, db: OpcDatabase): void {
|
|
46
|
+
api.registerTool(
|
|
47
|
+
{
|
|
48
|
+
name: "opc_acquisition",
|
|
49
|
+
label: "OPC 收并购管理",
|
|
50
|
+
description:
|
|
51
|
+
"收并购管理工具(资金闭环核心)。操作: " +
|
|
52
|
+
"create_acquisition(发起收购,记录亏损公司收购案例), " +
|
|
53
|
+
"list_acquisitions(收并购列表,可按状态/公司筛选), " +
|
|
54
|
+
"update_acquisition(更新案例状态/价格/税务抵扣), " +
|
|
55
|
+
"acquisition_summary(平台收并购汇总:总收购数、累计亏损、税务优化总额)",
|
|
56
|
+
parameters: AcquisitionSchema,
|
|
57
|
+
async execute(_toolCallId, params) {
|
|
58
|
+
const p = params as AcquisitionParams;
|
|
59
|
+
try {
|
|
60
|
+
switch (p.action) {
|
|
61
|
+
case "create_acquisition": {
|
|
62
|
+
const id = db.genId();
|
|
63
|
+
const now = new Date().toISOString();
|
|
64
|
+
const lossAmount = p.loss_amount ?? 0;
|
|
65
|
+
// 粗算税务抵扣:亏损 × 25% 企业所得税率
|
|
66
|
+
const taxDeduction = lossAmount * 0.25;
|
|
67
|
+
|
|
68
|
+
db.execute(
|
|
69
|
+
`INSERT INTO opc_acquisition_cases
|
|
70
|
+
(id, company_id, acquirer_id, case_type, status, trigger_reason,
|
|
71
|
+
acquisition_price, loss_amount, tax_deduction, initiated_date, notes, created_at, updated_at)
|
|
72
|
+
VALUES (?, ?, 'starriver', 'acquisition', 'evaluating', ?, ?, ?, ?, date('now'), ?, ?, ?)`,
|
|
73
|
+
id, p.company_id, p.trigger_reason,
|
|
74
|
+
p.acquisition_price, lossAmount, taxDeduction,
|
|
75
|
+
p.notes ?? "", now, now,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// 同步将公司状态标记为 acquired
|
|
79
|
+
db.execute(
|
|
80
|
+
`UPDATE opc_companies SET status = 'acquired', updated_at = ? WHERE id = ?`,
|
|
81
|
+
now, p.company_id,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const row = db.queryOne(
|
|
85
|
+
`SELECT a.*, c.name as company_name FROM opc_acquisition_cases a
|
|
86
|
+
LEFT JOIN opc_companies c ON a.company_id = c.id
|
|
87
|
+
WHERE a.id = ?`,
|
|
88
|
+
id,
|
|
89
|
+
);
|
|
90
|
+
return json({ ok: true, case: row, note: `税务优化估算:亏损 ${lossAmount.toLocaleString()} 元 × 25% = 可抵扣 ${taxDeduction.toLocaleString()} 元` });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case "list_acquisitions": {
|
|
94
|
+
let sql = `SELECT a.*, c.name as company_name, c.industry
|
|
95
|
+
FROM opc_acquisition_cases a
|
|
96
|
+
LEFT JOIN opc_companies c ON a.company_id = c.id
|
|
97
|
+
WHERE 1=1`;
|
|
98
|
+
const vals: unknown[] = [];
|
|
99
|
+
if (p.status) { sql += " AND a.status = ?"; vals.push(p.status); }
|
|
100
|
+
if (p.company_id) { sql += " AND a.company_id = ?"; vals.push(p.company_id); }
|
|
101
|
+
sql += " ORDER BY a.created_at DESC";
|
|
102
|
+
return json(db.query(sql, ...vals));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case "update_acquisition": {
|
|
106
|
+
const sets: string[] = ["updated_at = ?"];
|
|
107
|
+
const vals: unknown[] = [new Date().toISOString()];
|
|
108
|
+
if (p.status !== undefined) { sets.push("status = ?"); vals.push(p.status); }
|
|
109
|
+
if (p.acquisition_price !== undefined) { sets.push("acquisition_price = ?"); vals.push(p.acquisition_price); }
|
|
110
|
+
if (p.loss_amount !== undefined) { sets.push("loss_amount = ?"); vals.push(p.loss_amount); }
|
|
111
|
+
if (p.tax_deduction !== undefined) { sets.push("tax_deduction = ?"); vals.push(p.tax_deduction); }
|
|
112
|
+
if (p.closed_date !== undefined) { sets.push("closed_date = ?"); vals.push(p.closed_date); }
|
|
113
|
+
if (p.notes !== undefined) { sets.push("notes = ?"); vals.push(p.notes); }
|
|
114
|
+
vals.push(p.case_id);
|
|
115
|
+
db.execute(`UPDATE opc_acquisition_cases SET ${sets.join(", ")} WHERE id = ?`, ...vals);
|
|
116
|
+
return json(db.queryOne(
|
|
117
|
+
`SELECT a.*, c.name as company_name FROM opc_acquisition_cases a
|
|
118
|
+
LEFT JOIN opc_companies c ON a.company_id = c.id WHERE a.id = ?`,
|
|
119
|
+
p.case_id,
|
|
120
|
+
));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case "acquisition_summary": {
|
|
124
|
+
const summary = db.queryOne(
|
|
125
|
+
`SELECT
|
|
126
|
+
COUNT(*) as total_cases,
|
|
127
|
+
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed,
|
|
128
|
+
COUNT(CASE WHEN status = 'in_progress' THEN 1 END) as in_progress,
|
|
129
|
+
COUNT(CASE WHEN status = 'evaluating' THEN 1 END) as evaluating,
|
|
130
|
+
COALESCE(SUM(acquisition_price), 0) as total_acquisition_cost,
|
|
131
|
+
COALESCE(SUM(loss_amount), 0) as total_loss_amount,
|
|
132
|
+
COALESCE(SUM(tax_deduction), 0) as total_tax_deduction
|
|
133
|
+
FROM opc_acquisition_cases`,
|
|
134
|
+
);
|
|
135
|
+
return json(summary);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
default:
|
|
139
|
+
return json({ error: `未知操作: ${(p as { action: string }).action}` });
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{ name: "opc_acquisition" },
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
api.logger.info("opc: 已注册 opc_acquisition 工具");
|
|
150
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — opc_asset_package 资产包管理工具
|
|
3
|
+
*
|
|
4
|
+
* 资金闭环第二阶段:将收并购回来的公司整合打包,
|
|
5
|
+
* 形成具有真实运营数据和科创属性的"资产包",
|
|
6
|
+
* 转让给城投公司,协助城投申请科创贷,并收取融资服务费。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
10
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
11
|
+
import type { OpcDatabase } from "../db/index.js";
|
|
12
|
+
import { json } from "../utils/tool-helper.js";
|
|
13
|
+
|
|
14
|
+
const AssetPackageSchema = Type.Union([
|
|
15
|
+
// ── 资产包管理 ──
|
|
16
|
+
Type.Object({
|
|
17
|
+
action: Type.Literal("create_asset_package"),
|
|
18
|
+
name: Type.String({ description: "资产包名称,如「仁和区2026Q1科创资产包」" }),
|
|
19
|
+
description: Type.Optional(Type.String({ description: "资产包描述,包含科创属性说明" })),
|
|
20
|
+
notes: Type.Optional(Type.String({ description: "备注" })),
|
|
21
|
+
}),
|
|
22
|
+
Type.Object({
|
|
23
|
+
action: Type.Literal("add_company_to_package"),
|
|
24
|
+
package_id: Type.String({ description: "资产包 ID" }),
|
|
25
|
+
company_id: Type.String({ description: "要加入的公司 ID(应为已收并购状态)" }),
|
|
26
|
+
acquisition_case_id: Type.Optional(Type.String({ description: "关联的收并购案例 ID" })),
|
|
27
|
+
valuation: Type.Number({ description: "该公司在资产包中的估值(元)" }),
|
|
28
|
+
}),
|
|
29
|
+
Type.Object({
|
|
30
|
+
action: Type.Literal("list_asset_packages"),
|
|
31
|
+
status: Type.Optional(Type.String({ description: "按状态筛选: assembling/ready/transferred/closed" })),
|
|
32
|
+
}),
|
|
33
|
+
Type.Object({
|
|
34
|
+
action: Type.Literal("get_package_detail"),
|
|
35
|
+
package_id: Type.String({ description: "资产包 ID" }),
|
|
36
|
+
}),
|
|
37
|
+
Type.Object({
|
|
38
|
+
action: Type.Literal("update_package"),
|
|
39
|
+
package_id: Type.String({ description: "资产包 ID" }),
|
|
40
|
+
status: Type.Optional(Type.String({ description: "新状态: assembling/ready/transferred/closed" })),
|
|
41
|
+
sci_tech_certified: Type.Optional(Type.Number({ description: "科创认定企业数量" })),
|
|
42
|
+
assembled_date: Type.Optional(Type.String({ description: "打包完成日期 (YYYY-MM-DD)" })),
|
|
43
|
+
notes: Type.Optional(Type.String({ description: "备注" })),
|
|
44
|
+
}),
|
|
45
|
+
// ── 城投转让 ──
|
|
46
|
+
Type.Object({
|
|
47
|
+
action: Type.Literal("create_ct_transfer"),
|
|
48
|
+
package_id: Type.String({ description: "资产包 ID" }),
|
|
49
|
+
ct_company: Type.String({ description: "城投公司名称,如「仁和工发集团」" }),
|
|
50
|
+
transfer_price: Type.Number({ description: "资产包转让价格(元)" }),
|
|
51
|
+
sci_loan_target: Type.Optional(Type.Number({ description: "城投目标科创贷金额(元)" })),
|
|
52
|
+
transfer_date: Type.Optional(Type.String({ description: "转让日期 (YYYY-MM-DD)" })),
|
|
53
|
+
notes: Type.Optional(Type.String({ description: "备注" })),
|
|
54
|
+
}),
|
|
55
|
+
Type.Object({
|
|
56
|
+
action: Type.Literal("update_ct_transfer"),
|
|
57
|
+
transfer_id: Type.String({ description: "城投转让记录 ID" }),
|
|
58
|
+
status: Type.Optional(Type.String({ description: "新状态: negotiating/signed/completed/cancelled" })),
|
|
59
|
+
sci_loan_actual: Type.Optional(Type.Number({ description: "城投实际获得科创贷金额(元)" })),
|
|
60
|
+
loan_date: Type.Optional(Type.String({ description: "放款日期 (YYYY-MM-DD)" })),
|
|
61
|
+
notes: Type.Optional(Type.String({ description: "备注" })),
|
|
62
|
+
}),
|
|
63
|
+
Type.Object({
|
|
64
|
+
action: Type.Literal("list_ct_transfers"),
|
|
65
|
+
status: Type.Optional(Type.String({ description: "按状态筛选" })),
|
|
66
|
+
}),
|
|
67
|
+
// ── 融资服务费 ──
|
|
68
|
+
Type.Object({
|
|
69
|
+
action: Type.Literal("record_financing_fee"),
|
|
70
|
+
transfer_id: Type.String({ description: "城投转让记录 ID" }),
|
|
71
|
+
base_amount: Type.Number({ description: "计费基数(通常为科创贷实际金额,元)" }),
|
|
72
|
+
fee_rate: Type.Number({ description: "服务费率(%),通常 1%-3%" }),
|
|
73
|
+
notes: Type.Optional(Type.String({ description: "备注" })),
|
|
74
|
+
}),
|
|
75
|
+
Type.Object({
|
|
76
|
+
action: Type.Literal("update_financing_fee"),
|
|
77
|
+
fee_id: Type.String({ description: "融资服务费记录 ID" }),
|
|
78
|
+
status: Type.Optional(Type.String({ description: "新状态: pending/invoiced/paid" })),
|
|
79
|
+
invoiced: Type.Optional(Type.Number({ description: "是否已开票: 1=是, 0=否" })),
|
|
80
|
+
paid_date: Type.Optional(Type.String({ description: "收款日期 (YYYY-MM-DD)" })),
|
|
81
|
+
notes: Type.Optional(Type.String({ description: "备注" })),
|
|
82
|
+
}),
|
|
83
|
+
// ── 汇总报表 ──
|
|
84
|
+
Type.Object({
|
|
85
|
+
action: Type.Literal("closure_summary"),
|
|
86
|
+
description: Type.Optional(Type.String({ description: "资金闭环整体汇总:资产包数、城投转让总额、科创贷总额、融资服务费总收入" })),
|
|
87
|
+
}),
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
type AssetPackageParams = Static<typeof AssetPackageSchema>;
|
|
91
|
+
|
|
92
|
+
export function registerAssetPackageTool(api: OpenClawPluginApi, db: OpcDatabase): void {
|
|
93
|
+
api.registerTool(
|
|
94
|
+
{
|
|
95
|
+
name: "opc_asset_package",
|
|
96
|
+
label: "OPC 资产包与城投转让",
|
|
97
|
+
description:
|
|
98
|
+
"资产包管理与城投转让工具(资金闭环核心)。操作: " +
|
|
99
|
+
"create_asset_package(创建资产包), add_company_to_package(将已收并购公司加入资产包), " +
|
|
100
|
+
"list_asset_packages(资产包列表), get_package_detail(资产包详情+成员公司), update_package(更新状态/科创认定数), " +
|
|
101
|
+
"create_ct_transfer(发起城投转让), update_ct_transfer(更新转让状态/科创贷金额), list_ct_transfers(转让记录列表), " +
|
|
102
|
+
"record_financing_fee(记录融资服务费), update_financing_fee(更新收款状态), " +
|
|
103
|
+
"closure_summary(资金闭环汇总报表)",
|
|
104
|
+
parameters: AssetPackageSchema,
|
|
105
|
+
async execute(_toolCallId, params) {
|
|
106
|
+
const p = params as AssetPackageParams;
|
|
107
|
+
try {
|
|
108
|
+
switch (p.action) {
|
|
109
|
+
case "create_asset_package": {
|
|
110
|
+
const id = db.genId();
|
|
111
|
+
const now = new Date().toISOString();
|
|
112
|
+
db.execute(
|
|
113
|
+
`INSERT INTO opc_asset_packages
|
|
114
|
+
(id, name, description, status, total_valuation, company_count, sci_tech_certified, notes, created_at, updated_at)
|
|
115
|
+
VALUES (?, ?, ?, 'assembling', 0, 0, 0, ?, ?, ?)`,
|
|
116
|
+
id, p.name, p.description ?? "", p.notes ?? "", now, now,
|
|
117
|
+
);
|
|
118
|
+
return json({ ok: true, package: db.queryOne("SELECT * FROM opc_asset_packages WHERE id = ?", id) });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case "add_company_to_package": {
|
|
122
|
+
const id = db.genId();
|
|
123
|
+
const now = new Date().toISOString();
|
|
124
|
+
db.execute(
|
|
125
|
+
`INSERT INTO opc_asset_package_items (id, package_id, company_id, acquisition_case_id, valuation, created_at)
|
|
126
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
127
|
+
id, p.package_id, p.company_id, p.acquisition_case_id ?? "", p.valuation, now,
|
|
128
|
+
);
|
|
129
|
+
// 更新资产包汇总
|
|
130
|
+
db.execute(
|
|
131
|
+
`UPDATE opc_asset_packages
|
|
132
|
+
SET company_count = (SELECT COUNT(*) FROM opc_asset_package_items WHERE package_id = ?),
|
|
133
|
+
total_valuation = (SELECT COALESCE(SUM(valuation),0) FROM opc_asset_package_items WHERE package_id = ?),
|
|
134
|
+
updated_at = ?
|
|
135
|
+
WHERE id = ?`,
|
|
136
|
+
p.package_id, p.package_id, now, p.package_id,
|
|
137
|
+
);
|
|
138
|
+
const pkg = db.queryOne("SELECT * FROM opc_asset_packages WHERE id = ?", p.package_id);
|
|
139
|
+
return json({ ok: true, item_id: id, package: pkg });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case "list_asset_packages": {
|
|
143
|
+
let sql = "SELECT * FROM opc_asset_packages WHERE 1=1";
|
|
144
|
+
const vals: unknown[] = [];
|
|
145
|
+
if (p.status) { sql += " AND status = ?"; vals.push(p.status); }
|
|
146
|
+
sql += " ORDER BY created_at DESC";
|
|
147
|
+
return json(db.query(sql, ...vals));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
case "get_package_detail": {
|
|
151
|
+
const pkg = db.queryOne("SELECT * FROM opc_asset_packages WHERE id = ?", p.package_id);
|
|
152
|
+
if (!pkg) return json({ error: "资产包不存在" });
|
|
153
|
+
const items = db.query(
|
|
154
|
+
`SELECT i.*, c.name as company_name, c.industry, c.status as company_status
|
|
155
|
+
FROM opc_asset_package_items i
|
|
156
|
+
LEFT JOIN opc_companies c ON i.company_id = c.id
|
|
157
|
+
WHERE i.package_id = ?
|
|
158
|
+
ORDER BY i.created_at`,
|
|
159
|
+
p.package_id,
|
|
160
|
+
);
|
|
161
|
+
const transfers = db.query(
|
|
162
|
+
"SELECT * FROM opc_ct_transfers WHERE package_id = ? ORDER BY created_at DESC",
|
|
163
|
+
p.package_id,
|
|
164
|
+
);
|
|
165
|
+
return json({ package: pkg, companies: items, transfers });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
case "update_package": {
|
|
169
|
+
const sets: string[] = ["updated_at = ?"];
|
|
170
|
+
const vals: unknown[] = [new Date().toISOString()];
|
|
171
|
+
if (p.status !== undefined) { sets.push("status = ?"); vals.push(p.status); }
|
|
172
|
+
if (p.sci_tech_certified !== undefined) { sets.push("sci_tech_certified = ?"); vals.push(p.sci_tech_certified); }
|
|
173
|
+
if (p.assembled_date !== undefined) { sets.push("assembled_date = ?"); vals.push(p.assembled_date); }
|
|
174
|
+
if (p.notes !== undefined) { sets.push("notes = ?"); vals.push(p.notes); }
|
|
175
|
+
vals.push(p.package_id);
|
|
176
|
+
db.execute(`UPDATE opc_asset_packages SET ${sets.join(", ")} WHERE id = ?`, ...vals);
|
|
177
|
+
return json(db.queryOne("SELECT * FROM opc_asset_packages WHERE id = ?", p.package_id));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case "create_ct_transfer": {
|
|
181
|
+
const id = db.genId();
|
|
182
|
+
const now = new Date().toISOString();
|
|
183
|
+
db.execute(
|
|
184
|
+
`INSERT INTO opc_ct_transfers
|
|
185
|
+
(id, package_id, ct_company, transfer_price, status, sci_loan_target, sci_loan_actual, transfer_date, notes, created_at, updated_at)
|
|
186
|
+
VALUES (?, ?, ?, ?, 'negotiating', ?, 0, ?, ?, ?, ?)`,
|
|
187
|
+
id, p.package_id, p.ct_company, p.transfer_price,
|
|
188
|
+
p.sci_loan_target ?? 0, p.transfer_date ?? "", p.notes ?? "", now, now,
|
|
189
|
+
);
|
|
190
|
+
// 更新资产包状态
|
|
191
|
+
db.execute("UPDATE opc_asset_packages SET status = 'transferred', updated_at = ? WHERE id = ?", now, p.package_id);
|
|
192
|
+
return json({ ok: true, transfer: db.queryOne("SELECT * FROM opc_ct_transfers WHERE id = ?", id) });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
case "update_ct_transfer": {
|
|
196
|
+
const sets: string[] = ["updated_at = ?"];
|
|
197
|
+
const vals: unknown[] = [new Date().toISOString()];
|
|
198
|
+
if (p.status !== undefined) { sets.push("status = ?"); vals.push(p.status); }
|
|
199
|
+
if (p.sci_loan_actual !== undefined) { sets.push("sci_loan_actual = ?"); vals.push(p.sci_loan_actual); }
|
|
200
|
+
if (p.loan_date !== undefined) { sets.push("loan_date = ?"); vals.push(p.loan_date); }
|
|
201
|
+
if (p.notes !== undefined) { sets.push("notes = ?"); vals.push(p.notes); }
|
|
202
|
+
vals.push(p.transfer_id);
|
|
203
|
+
db.execute(`UPDATE opc_ct_transfers SET ${sets.join(", ")} WHERE id = ?`, ...vals);
|
|
204
|
+
return json(db.queryOne("SELECT * FROM opc_ct_transfers WHERE id = ?", p.transfer_id));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
case "list_ct_transfers": {
|
|
208
|
+
let sql = `SELECT t.*, p.name as package_name, p.company_count
|
|
209
|
+
FROM opc_ct_transfers t
|
|
210
|
+
LEFT JOIN opc_asset_packages p ON t.package_id = p.id
|
|
211
|
+
WHERE 1=1`;
|
|
212
|
+
const vals: unknown[] = [];
|
|
213
|
+
if (p.status) { sql += " AND t.status = ?"; vals.push(p.status); }
|
|
214
|
+
sql += " ORDER BY t.created_at DESC";
|
|
215
|
+
return json(db.query(sql, ...vals));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
case "record_financing_fee": {
|
|
219
|
+
const id = db.genId();
|
|
220
|
+
const now = new Date().toISOString();
|
|
221
|
+
const feeAmount = p.base_amount * (p.fee_rate / 100);
|
|
222
|
+
db.execute(
|
|
223
|
+
`INSERT INTO opc_financing_fees
|
|
224
|
+
(id, transfer_id, fee_rate, fee_amount, base_amount, status, invoiced, notes, created_at, updated_at)
|
|
225
|
+
VALUES (?, ?, ?, ?, ?, 'pending', 0, ?, ?, ?)`,
|
|
226
|
+
id, p.transfer_id, p.fee_rate, feeAmount, p.base_amount, p.notes ?? "", now, now,
|
|
227
|
+
);
|
|
228
|
+
return json({
|
|
229
|
+
ok: true,
|
|
230
|
+
fee: db.queryOne("SELECT * FROM opc_financing_fees WHERE id = ?", id),
|
|
231
|
+
calculation: `${p.base_amount.toLocaleString()} 元 × ${p.fee_rate}% = ${feeAmount.toLocaleString()} 元`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
case "update_financing_fee": {
|
|
236
|
+
const sets: string[] = ["updated_at = ?"];
|
|
237
|
+
const vals: unknown[] = [new Date().toISOString()];
|
|
238
|
+
if (p.status !== undefined) { sets.push("status = ?"); vals.push(p.status); }
|
|
239
|
+
if (p.invoiced !== undefined) { sets.push("invoiced = ?"); vals.push(p.invoiced); }
|
|
240
|
+
if (p.paid_date !== undefined) { sets.push("paid_date = ?"); vals.push(p.paid_date); }
|
|
241
|
+
if (p.notes !== undefined) { sets.push("notes = ?"); vals.push(p.notes); }
|
|
242
|
+
vals.push(p.fee_id);
|
|
243
|
+
db.execute(`UPDATE opc_financing_fees SET ${sets.join(", ")} WHERE id = ?`, ...vals);
|
|
244
|
+
return json(db.queryOne("SELECT * FROM opc_financing_fees WHERE id = ?", p.fee_id));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
case "closure_summary": {
|
|
248
|
+
const packages = db.queryOne(
|
|
249
|
+
`SELECT COUNT(*) as total, COUNT(CASE WHEN status='transferred' THEN 1 END) as transferred,
|
|
250
|
+
COALESCE(SUM(total_valuation),0) as total_valuation,
|
|
251
|
+
COALESCE(SUM(company_count),0) as total_companies,
|
|
252
|
+
COALESCE(SUM(sci_tech_certified),0) as sci_tech_certified
|
|
253
|
+
FROM opc_asset_packages`,
|
|
254
|
+
);
|
|
255
|
+
const transfers = db.queryOne(
|
|
256
|
+
`SELECT COUNT(*) as total,
|
|
257
|
+
COALESCE(SUM(transfer_price),0) as total_transfer_price,
|
|
258
|
+
COALESCE(SUM(sci_loan_target),0) as total_loan_target,
|
|
259
|
+
COALESCE(SUM(sci_loan_actual),0) as total_loan_actual
|
|
260
|
+
FROM opc_ct_transfers`,
|
|
261
|
+
);
|
|
262
|
+
const fees = db.queryOne(
|
|
263
|
+
`SELECT COUNT(*) as total,
|
|
264
|
+
COALESCE(SUM(fee_amount),0) as total_fee,
|
|
265
|
+
COALESCE(SUM(CASE WHEN status='paid' THEN fee_amount ELSE 0 END),0) as collected_fee
|
|
266
|
+
FROM opc_financing_fees`,
|
|
267
|
+
);
|
|
268
|
+
return json({ asset_packages: packages, ct_transfers: transfers, financing_fees: fees });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
default:
|
|
272
|
+
return json({ error: `未知操作: ${(p as { action: string }).action}` });
|
|
273
|
+
}
|
|
274
|
+
} catch (err) {
|
|
275
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{ name: "opc_asset_package" },
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
api.logger.info("opc: 已注册 opc_asset_package 工具");
|
|
283
|
+
}
|