@huo15/openclaw-enhance 1.0.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.
@@ -0,0 +1,158 @@
1
+ /**
2
+ * 模块5: 增强仪表盘
3
+ *
4
+ * 通过 gateway HTTP 路由提供可视化状态页面
5
+ */
6
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
7
+ import { getDb, getMemoryStats, getSafetyStats, getRecentMemories, getRecentSafetyEvents } from "../utils/sqlite-store.js";
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import type { DashboardConfig, Workflow } from "../types.js";
11
+
12
+ function loadWorkflows(openclawDir: string): Workflow[] {
13
+ const path = join(openclawDir, "memory", "enhance-workflows.json");
14
+ if (!existsSync(path)) return [];
15
+ try {
16
+ return JSON.parse(readFileSync(path, "utf-8"));
17
+ } catch {
18
+ return [];
19
+ }
20
+ }
21
+
22
+ const DASHBOARD_HTML = `<!DOCTYPE html>
23
+ <html lang="zh-CN">
24
+ <head>
25
+ <meta charset="utf-8">
26
+ <meta name="viewport" content="width=device-width,initial-scale=1">
27
+ <title>龙虾增强包 — 仪表盘</title>
28
+ <style>
29
+ *{margin:0;padding:0;box-sizing:border-box}
30
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0f1117;color:#e0e0e0;padding:24px;max-width:960px;margin:0 auto}
31
+ h1{font-size:1.8em;margin-bottom:8px;color:#ff6b35}
32
+ .subtitle{color:#888;margin-bottom:24px;font-size:0.95em}
33
+ .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:32px}
34
+ .card{background:#1a1d27;border-radius:12px;padding:20px;border:1px solid #2a2d37}
35
+ .card h3{font-size:0.85em;color:#888;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px}
36
+ .card .value{font-size:2em;font-weight:700;color:#ff6b35}
37
+ .card .label{font-size:0.8em;color:#666;margin-top:4px}
38
+ .section{margin-bottom:32px}
39
+ .section h2{font-size:1.2em;margin-bottom:12px;color:#ccc;border-bottom:1px solid #2a2d37;padding-bottom:8px}
40
+ table{width:100%;border-collapse:collapse}
41
+ th,td{text-align:left;padding:8px 12px;border-bottom:1px solid #1a1d27}
42
+ th{color:#888;font-size:0.8em;text-transform:uppercase;letter-spacing:1px}
43
+ td{font-size:0.9em}
44
+ .badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.75em;font-weight:600}
45
+ .badge-block{background:#ff4444;color:#fff}
46
+ .badge-log{background:#444;color:#ccc}
47
+ .badge-allow{background:#2a5a2a;color:#8f8}
48
+ .empty{color:#555;font-style:italic;padding:16px 0}
49
+ footer{text-align:center;color:#444;font-size:0.8em;margin-top:40px}
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <h1>🦞 龙虾增强包</h1>
54
+ <p class="subtitle">OpenClaw Enhancement Kit Dashboard</p>
55
+
56
+ <div class="grid" id="stats"></div>
57
+
58
+ <div class="section">
59
+ <h2>最近记忆</h2>
60
+ <div id="memories"></div>
61
+ </div>
62
+
63
+ <div class="section">
64
+ <h2>安全事件</h2>
65
+ <div id="safety"></div>
66
+ </div>
67
+
68
+ <div class="section">
69
+ <h2>工作流</h2>
70
+ <div id="workflows"></div>
71
+ </div>
72
+
73
+ <footer>龙虾增强包 v1.0.0 — 非侵入式增强</footer>
74
+
75
+ <script>
76
+ async function load(){
77
+ try{
78
+ const r=await fetch('/enhance/api/status');
79
+ const d=await r.json();
80
+ // Stats cards
81
+ const s=document.getElementById('stats');
82
+ s.innerHTML=[
83
+ card('记忆总数',d.memory.total,'条'),
84
+ card('用户记忆',d.memory.user||0,'条'),
85
+ card('项目记忆',d.memory.project||0,'条'),
86
+ card('安全事件',d.safety.total,'次'),
87
+ card('已拦截',d.safety.blocked,'次'),
88
+ card('工作流',d.workflows.length,'个'),
89
+ ].join('');
90
+ // Memories table
91
+ const m=document.getElementById('memories');
92
+ if(d.recentMemories.length===0){m.innerHTML='<p class="empty">暂无记忆</p>';} else {
93
+ m.innerHTML='<table><tr><th>ID</th><th>类型</th><th>内容</th><th>时间</th></tr>'+
94
+ d.recentMemories.map(e=>'<tr><td>#'+e.id+'</td><td>'+e.category+'</td><td>'+esc(e.content).slice(0,60)+'</td><td>'+e.created_at+'</td></tr>').join('')+'</table>';
95
+ }
96
+ // Safety table
97
+ const sf=document.getElementById('safety');
98
+ if(d.recentSafety.length===0){sf.innerHTML='<p class="empty">暂无安全事件</p>';} else {
99
+ sf.innerHTML='<table><tr><th>动作</th><th>工具</th><th>参数</th><th>时间</th></tr>'+
100
+ d.recentSafety.map(e=>'<tr><td><span class="badge badge-'+e.action+'">'+e.action+'</span></td><td>'+e.tool+'</td><td>'+esc(e.params||'').slice(0,40)+'</td><td>'+e.created_at+'</td></tr>').join('')+'</table>';
101
+ }
102
+ // Workflows
103
+ const w=document.getElementById('workflows');
104
+ if(d.workflows.length===0){w.innerHTML='<p class="empty">暂无工作流</p>';} else {
105
+ w.innerHTML='<table><tr><th>名称</th><th>触发词</th><th>状态</th></tr>'+
106
+ d.workflows.map(e=>'<tr><td>'+esc(e.name)+'</td><td>'+esc(e.trigger)+'</td><td>'+(e.enabled?'✅':'⏸️')+'</td></tr>').join('')+'</table>';
107
+ }
108
+ }catch(e){document.body.innerHTML+='<p style="color:red">加载失败: '+e.message+'</p>';}
109
+ }
110
+ function card(t,v,l){return '<div class="card"><h3>'+t+'</h3><div class="value">'+v+'</div><div class="label">'+l+'</div></div>';}
111
+ function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
112
+ load();
113
+ </script>
114
+ </body>
115
+ </html>`;
116
+
117
+ export function registerDashboard(api: OpenClawPluginApi, _config?: DashboardConfig) {
118
+ const openclawDir = api.runtime.paths?.home ?? process.env.HOME + "/.openclaw";
119
+
120
+ // ── HTML 页面 ──
121
+ api.registerHttpRoute({
122
+ path: "/enhance",
123
+ match: "prefix",
124
+ auth: "gateway",
125
+ handler: async (req) => {
126
+ const url = new URL(req.url, "http://localhost");
127
+ const pathname = url.pathname;
128
+
129
+ // API: 状态数据
130
+ if (pathname === "/enhance/api/status") {
131
+ const db = getDb(openclawDir);
132
+ const memoryStats = getMemoryStats(db);
133
+ const safetyStats = getSafetyStats(db);
134
+ const recentMemories = getRecentMemories(db, 10);
135
+ const recentSafety = getRecentSafetyEvents(db, 10);
136
+ const workflows = loadWorkflows(openclawDir);
137
+
138
+ return new Response(
139
+ JSON.stringify({
140
+ memory: memoryStats,
141
+ safety: safetyStats,
142
+ recentMemories,
143
+ recentSafety,
144
+ workflows,
145
+ }),
146
+ { headers: { "Content-Type": "application/json" } },
147
+ );
148
+ }
149
+
150
+ // 默认: 仪表盘 HTML
151
+ return new Response(DASHBOARD_HTML, {
152
+ headers: { "Content-Type": "text/html; charset=utf-8" },
153
+ });
154
+ },
155
+ });
156
+
157
+ api.logger.info("[enhance] 仪表盘模块已加载,访问 /enhance/ 查看");
158
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * 模块3: 提示词增强
3
+ *
4
+ * 灵感来自 Claude Code 的模块化系统提示词构建:
5
+ * - systemPromptSections 模式
6
+ * - 任务分类
7
+ * - 响应质量指引
8
+ * - 记忆上下文注入
9
+ */
10
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
11
+ import type { PromptConfig, PromptSection } from "../types.js";
12
+
13
+ const SECTIONS: Record<PromptSection, string> = {
14
+ taskClassification: [
15
+ "## 任务分类(增强包)",
16
+ "在开始工作前,先判断当前请求的类型并调整策略:",
17
+ "- **编程**: 展示最小变更,解释为什么而非做了什么,先理解现有代码再修改",
18
+ "- **研究**: 区分事实与推测,引用来源,系统性搜索而非猜测",
19
+ "- **管理**: 外部操作前先确认,记录执行结果",
20
+ "- **对话**: 简洁直接,不过度解释",
21
+ ].join("\n"),
22
+
23
+ qualityGuidelines: [
24
+ "## 响应质量准则(增强包)",
25
+ "- 直接给出答案或行动,不要先复述用户说了什么",
26
+ "- 一句话能说清的不要用三句话",
27
+ "- 修 bug 不要顺手重构周围代码",
28
+ "- 不要添加请求之外的功能、注释或类型标注",
29
+ "- 不要为不可能发生的场景做错误处理",
30
+ "- 三行相似代码好过一个过早的抽象",
31
+ "- 失败时先诊断原因,不要盲目重试",
32
+ "- 做破坏性操作前先确认(删除、force push、覆盖未保存的修改)",
33
+ ].join("\n"),
34
+
35
+ memoryContext: "", // 由 structured-memory 模块通过自己的钩子注入
36
+
37
+ safetyAwareness: [
38
+ "## 安全意识(增强包)",
39
+ "- 写代码时注意 OWASP Top 10 漏洞(注入、XSS、SSRF 等)",
40
+ "- 不要提交 .env、密钥、凭据等敏感文件",
41
+ "- 执行系统命令时避免命令注入风险",
42
+ "- 如果发现自己写了不安全的代码,立即修复",
43
+ ].join("\n"),
44
+ };
45
+
46
+ export function registerPromptEnhancer(api: OpenClawPluginApi, config?: PromptConfig) {
47
+ const enabledSections: PromptSection[] = config?.sections ?? ["qualityGuidelines", "memoryContext"];
48
+
49
+ api.on("before_prompt_build", () => {
50
+ const parts: string[] = [];
51
+
52
+ for (const section of enabledSections) {
53
+ const content = SECTIONS[section];
54
+ if (content) parts.push(content);
55
+ }
56
+
57
+ if (parts.length === 0) return {};
58
+
59
+ return {
60
+ appendSystemContext: "\n\n" + parts.join("\n\n"),
61
+ };
62
+ });
63
+
64
+ api.logger.info(`[enhance] 提示词增强模块已加载,启用段落: ${enabledSections.join(", ")}`);
65
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * 模块1: 结构化记忆系统
3
+ *
4
+ * 灵感来自 Claude Code 的 auto-memory 系统:
5
+ * - 按类型分类: user / project / feedback / reference / decision
6
+ * - 自动注入相关记忆到上下文
7
+ * - 会话结束时自动提取关键事实
8
+ */
9
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
10
+ import { Type } from "@sinclair/typebox";
11
+ import {
12
+ getDb,
13
+ storeMemory,
14
+ searchMemories,
15
+ getRecentMemories,
16
+ deleteMemory,
17
+ getMemoryStats,
18
+ } from "../utils/sqlite-store.js";
19
+ import type { MemoryConfig, MemoryCategory } from "../types.js";
20
+
21
+ const VALID_CATEGORIES: MemoryCategory[] = ["user", "project", "feedback", "reference", "decision"];
22
+
23
+ export function registerStructuredMemory(api: OpenClawPluginApi, config?: MemoryConfig) {
24
+ const openclawDir = api.runtime.paths?.home ?? process.env.HOME + "/.openclaw";
25
+ const db = getDb(openclawDir);
26
+ const maxCtx = config?.maxContextEntries ?? 5;
27
+
28
+ // ── Tool: enhance_memory_store ──
29
+ api.registerTool({
30
+ name: "enhance_memory_store",
31
+ description: [
32
+ "存储一条结构化记忆。",
33
+ "分类说明:",
34
+ "- user: 用户偏好、角色、知识背景",
35
+ "- project: 项目状态、目标、决策、截止日期",
36
+ "- feedback: 用户对工作方式的反馈和纠正",
37
+ "- reference: 外部资源指针(链接、文档位置)",
38
+ "- decision: 重要的技术或业务决策及其原因",
39
+ "",
40
+ "使用场景:当你了解到值得在未来会话中记住的信息时调用此工具。",
41
+ "不要存储可以从代码或 git 历史推导出的信息。",
42
+ ].join("\n"),
43
+ parameters: Type.Object({
44
+ category: Type.Union(VALID_CATEGORIES.map((c) => Type.Literal(c)), {
45
+ description: "记忆类型: user|project|feedback|reference|decision",
46
+ }),
47
+ content: Type.String({ description: "记忆内容(建议中文)" }),
48
+ tags: Type.Optional(Type.String({ description: "逗号分隔的标签" })),
49
+ importance: Type.Optional(
50
+ Type.Number({ description: "重要性 1-10,默认 5", minimum: 1, maximum: 10 }),
51
+ ),
52
+ }),
53
+ async execute(_id, params) {
54
+ const entry = storeMemory(
55
+ db,
56
+ params.category as MemoryCategory,
57
+ params.content,
58
+ params.tags ?? "",
59
+ params.importance ?? 5,
60
+ );
61
+ return {
62
+ content: [
63
+ {
64
+ type: "text" as const,
65
+ text: `已存储记忆 #${entry.id} [${entry.category}]: ${entry.content.slice(0, 80)}...`,
66
+ },
67
+ ],
68
+ };
69
+ },
70
+ });
71
+
72
+ // ── Tool: enhance_memory_search ──
73
+ api.registerTool({
74
+ name: "enhance_memory_search",
75
+ description: [
76
+ "搜索结构化记忆。可按分类、关键词筛选。",
77
+ "用于在回答问题前查找相关上下文、用户偏好或历史决策。",
78
+ ].join("\n"),
79
+ parameters: Type.Object({
80
+ category: Type.Optional(
81
+ Type.Union(VALID_CATEGORIES.map((c) => Type.Literal(c)), {
82
+ description: "按分类筛选",
83
+ }),
84
+ ),
85
+ keyword: Type.Optional(Type.String({ description: "关键词搜索" })),
86
+ limit: Type.Optional(Type.Number({ description: "返回条数,默认 10", default: 10 })),
87
+ }),
88
+ async execute(_id, params) {
89
+ const entries = searchMemories(db, {
90
+ category: params.category as MemoryCategory | undefined,
91
+ keyword: params.keyword,
92
+ limit: params.limit ?? 10,
93
+ });
94
+ if (entries.length === 0) {
95
+ return { content: [{ type: "text" as const, text: "未找到匹配的记忆。" }] };
96
+ }
97
+ const lines = entries.map(
98
+ (e) =>
99
+ `#${e.id} [${e.category}] (重要性:${e.importance}) ${e.created_at}\n ${e.content}\n 标签: ${e.tags || "无"}`,
100
+ );
101
+ return {
102
+ content: [{ type: "text" as const, text: `找到 ${entries.length} 条记忆:\n\n${lines.join("\n\n")}` }],
103
+ };
104
+ },
105
+ });
106
+
107
+ // ── Tool: enhance_memory_review ──
108
+ api.registerTool({
109
+ name: "enhance_memory_review",
110
+ description: "查看记忆统计和最近记忆,用于整理和清理。也可删除指定记忆。",
111
+ parameters: Type.Object({
112
+ action: Type.Union([Type.Literal("stats"), Type.Literal("recent"), Type.Literal("delete")], {
113
+ description: "操作: stats(统计) / recent(最近) / delete(删除)",
114
+ }),
115
+ id: Type.Optional(Type.Number({ description: "要删除的记忆 ID(action=delete 时必填)" })),
116
+ limit: Type.Optional(Type.Number({ description: "recent 模式返回条数,默认 10" })),
117
+ }),
118
+ async execute(_id, params) {
119
+ if (params.action === "stats") {
120
+ const stats = getMemoryStats(db);
121
+ const lines = Object.entries(stats).map(([k, v]) => `${k}: ${v}`);
122
+ return { content: [{ type: "text" as const, text: `记忆统计:\n${lines.join("\n")}` }] };
123
+ }
124
+ if (params.action === "delete") {
125
+ if (!params.id) {
126
+ return { content: [{ type: "text" as const, text: "删除操作需要提供记忆 ID。" }] };
127
+ }
128
+ const ok = deleteMemory(db, params.id);
129
+ return {
130
+ content: [{ type: "text" as const, text: ok ? `已删除记忆 #${params.id}` : `未找到记忆 #${params.id}` }],
131
+ };
132
+ }
133
+ // recent
134
+ const entries = getRecentMemories(db, params.limit ?? 10);
135
+ if (entries.length === 0) {
136
+ return { content: [{ type: "text" as const, text: "暂无记忆。" }] };
137
+ }
138
+ const lines = entries.map(
139
+ (e) => `#${e.id} [${e.category}] ${e.created_at}: ${e.content.slice(0, 100)}`,
140
+ );
141
+ return {
142
+ content: [{ type: "text" as const, text: `最近 ${entries.length} 条记忆:\n${lines.join("\n")}` }],
143
+ };
144
+ },
145
+ });
146
+
147
+ // ── Hook: before_prompt_build — 注入相关记忆到上下文 ──
148
+ api.on("before_prompt_build", () => {
149
+ const recent = getRecentMemories(db, maxCtx);
150
+ if (recent.length === 0) return {};
151
+
152
+ const memoryBlock = recent
153
+ .map((e) => `- [${e.category}] ${e.content}`)
154
+ .join("\n");
155
+
156
+ return {
157
+ appendSystemContext: [
158
+ "\n\n## 增强记忆上下文(来自龙虾增强包)",
159
+ "以下是最近存储的结构化记忆,可作为参考:",
160
+ memoryBlock,
161
+ "\n你可以使用 enhance_memory_store 工具存储新的重要信息,使用 enhance_memory_search 查找历史记忆。",
162
+ ].join("\n"),
163
+ };
164
+ });
165
+
166
+ api.logger.info("[enhance] 结构化记忆模块已加载");
167
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * 模块2: 工具安全守卫
3
+ *
4
+ * 灵感来自 Claude Code 的权限系统:
5
+ * - deny/allow/ask 规则
6
+ * - pre_tool_use 钩子验证
7
+ * - 安全审计日志
8
+ */
9
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
10
+ import { Type } from "@sinclair/typebox";
11
+ import { getDb, logSafetyEvent, getRecentSafetyEvents, getSafetyStats } from "../utils/sqlite-store.js";
12
+ import type { SafetyConfig, SafetyRule } from "../types.js";
13
+
14
+ /**
15
+ * 简单 glob 匹配(支持 * 通配符)
16
+ */
17
+ function matchGlob(pattern: string, text: string): boolean {
18
+ const regex = new RegExp(
19
+ "^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$",
20
+ "i",
21
+ );
22
+ return regex.test(text);
23
+ }
24
+
25
+ /**
26
+ * 从工具调用参数中提取可匹配的文本
27
+ */
28
+ function extractMatchText(params: Record<string, unknown>): string {
29
+ // 常见参数名:command, input, path, file_path, url
30
+ for (const key of ["command", "input", "path", "file_path", "url", "query"]) {
31
+ if (typeof params[key] === "string") return params[key] as string;
32
+ }
33
+ return JSON.stringify(params);
34
+ }
35
+
36
+ function extractPathText(params: Record<string, unknown>): string {
37
+ for (const key of ["path", "file_path", "filePath", "filename"]) {
38
+ if (typeof params[key] === "string") return params[key] as string;
39
+ }
40
+ return "";
41
+ }
42
+
43
+ export function registerToolSafety(api: OpenClawPluginApi, config?: SafetyConfig) {
44
+ const openclawDir = api.runtime.paths?.home ?? process.env.HOME + "/.openclaw";
45
+ const db = getDb(openclawDir);
46
+ const rules: SafetyRule[] = config?.rules ?? [];
47
+ const defaultAction = config?.defaultAction ?? "allow";
48
+
49
+ // ── Hook: before_tool_call — 安全拦截 ──
50
+ api.on("before_tool_call", (_event, ctx) => {
51
+ const toolName = ctx?.toolName ?? "";
52
+ const params = (ctx?.params ?? {}) as Record<string, unknown>;
53
+ const matchText = extractMatchText(params);
54
+ const pathText = extractPathText(params);
55
+
56
+ for (const rule of rules) {
57
+ // 检查工具名匹配
58
+ if (!matchGlob(rule.tool, toolName)) continue;
59
+
60
+ // 检查命令/内容模式匹配
61
+ if (rule.pattern && !matchGlob(rule.pattern, matchText)) continue;
62
+
63
+ // 检查路径模式匹配
64
+ if (rule.pathPattern && !matchGlob(rule.pathPattern, pathText)) continue;
65
+
66
+ // 规则命中
67
+ logSafetyEvent(db, toolName, matchText.slice(0, 500), rule.action, JSON.stringify(rule), rule.reason ?? "");
68
+
69
+ if (rule.action === "block") {
70
+ api.logger.warn(`[enhance-safety] 已拦截: ${toolName} — ${rule.reason ?? "匹配安全规则"}`);
71
+ return { block: true };
72
+ }
73
+ // action === "log": 记录但不阻止
74
+ return {};
75
+ }
76
+
77
+ // 默认策略
78
+ if (defaultAction === "log") {
79
+ logSafetyEvent(db, toolName, matchText.slice(0, 500), "log", "", "default-log");
80
+ }
81
+ return {};
82
+ });
83
+
84
+ // ── Tool: enhance_safety_log — 查看安全日志 ──
85
+ api.registerTool({
86
+ name: "enhance_safety_log",
87
+ description: "查看工具安全审计日志和统计信息。",
88
+ parameters: Type.Object({
89
+ action: Type.Union([Type.Literal("recent"), Type.Literal("stats")], {
90
+ description: "recent: 最近事件 / stats: 统计摘要",
91
+ }),
92
+ limit: Type.Optional(Type.Number({ description: "返回条数(仅 recent 模式)", default: 15 })),
93
+ }),
94
+ async execute(_id, params) {
95
+ if (params.action === "stats") {
96
+ const stats = getSafetyStats(db);
97
+ return {
98
+ content: [
99
+ {
100
+ type: "text" as const,
101
+ text: `安全统计:\n总事件: ${stats.total}\n已拦截: ${stats.blocked}\n已记录: ${stats.logged}`,
102
+ },
103
+ ],
104
+ };
105
+ }
106
+ const events = getRecentSafetyEvents(db, params.limit ?? 15);
107
+ if (events.length === 0) {
108
+ return { content: [{ type: "text" as const, text: "暂无安全事件。" }] };
109
+ }
110
+ const lines = events.map(
111
+ (e) => `[${e.action}] ${e.created_at} | ${e.tool}: ${(e.params ?? "").slice(0, 60)}${e.reason ? ` (${e.reason})` : ""}`,
112
+ );
113
+ return { content: [{ type: "text" as const, text: `最近安全事件:\n${lines.join("\n")}` }] };
114
+ },
115
+ });
116
+
117
+ // ── Tool: enhance_safety_rules — 查看当前安全规则 ──
118
+ api.registerTool({
119
+ name: "enhance_safety_rules",
120
+ description: "查看当前配置的工具安全规则。",
121
+ parameters: Type.Object({}),
122
+ async execute() {
123
+ if (rules.length === 0) {
124
+ return {
125
+ content: [
126
+ {
127
+ type: "text" as const,
128
+ text: `当前无自定义安全规则。默认策略: ${defaultAction}\n\n可在 openclaw.json → plugins.entries.enhance.config.safety.rules 中配置。`,
129
+ },
130
+ ],
131
+ };
132
+ }
133
+ const lines = rules.map(
134
+ (r, i) =>
135
+ `${i + 1}. [${r.action}] tool=${r.tool}${r.pattern ? ` pattern="${r.pattern}"` : ""}${r.pathPattern ? ` path="${r.pathPattern}"` : ""}${r.reason ? ` — ${r.reason}` : ""}`,
136
+ );
137
+ return {
138
+ content: [
139
+ {
140
+ type: "text" as const,
141
+ text: `安全规则 (${rules.length} 条):\n${lines.join("\n")}\n\n默认策略: ${defaultAction}`,
142
+ },
143
+ ],
144
+ };
145
+ },
146
+ });
147
+
148
+ api.logger.info(`[enhance] 工具安全模块已加载,${rules.length} 条规则,默认策略: ${defaultAction}`);
149
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * 模块4: 工作流自动化
3
+ *
4
+ * 灵感来自 Claude Code 的 pre/post 钩子事件驱动工作流。
5
+ * 工作流 = 触发条件 + 提示词注入(不是代码执行)
6
+ */
7
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
8
+ import { Type } from "@sinclair/typebox";
9
+ import { join } from "node:path";
10
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
+ import type { Workflow, WorkflowConfig } from "../types.js";
12
+
13
+ function getWorkflowsPath(openclawDir: string): string {
14
+ return join(openclawDir, "memory", "enhance-workflows.json");
15
+ }
16
+
17
+ function loadWorkflows(openclawDir: string): Workflow[] {
18
+ const path = getWorkflowsPath(openclawDir);
19
+ if (!existsSync(path)) return [];
20
+ try {
21
+ return JSON.parse(readFileSync(path, "utf-8"));
22
+ } catch {
23
+ return [];
24
+ }
25
+ }
26
+
27
+ function saveWorkflows(openclawDir: string, workflows: Workflow[]): void {
28
+ writeFileSync(getWorkflowsPath(openclawDir), JSON.stringify(workflows, null, 2), "utf-8");
29
+ }
30
+
31
+ export function registerWorkflowHooks(api: OpenClawPluginApi, _config?: WorkflowConfig) {
32
+ const openclawDir = api.runtime.paths?.home ?? process.env.HOME + "/.openclaw";
33
+
34
+ // ── Tool: enhance_workflow_define ──
35
+ api.registerTool({
36
+ name: "enhance_workflow_define",
37
+ description: [
38
+ "定义一个工作流自动化规则。",
39
+ "工作流是「触发词 → 行为指令」的映射。",
40
+ "当用户消息包含触发词时,对应指令会被注入到系统提示中引导你的行为。",
41
+ "",
42
+ "示例:",
43
+ " trigger: '部署'",
44
+ " instructions: '先运行 git status 查看状态,然后询问用户确认,最后执行 ./deploy.sh'",
45
+ ].join("\n"),
46
+ parameters: Type.Object({
47
+ name: Type.String({ description: "工作流名称" }),
48
+ trigger: Type.String({ description: "触发词(出现在用户消息中即触发)" }),
49
+ instructions: Type.String({ description: "触发后注入的行为指令" }),
50
+ }),
51
+ async execute(_id, params) {
52
+ const workflows = loadWorkflows(openclawDir);
53
+ const existing = workflows.findIndex((w) => w.name === params.name);
54
+
55
+ const workflow: Workflow = {
56
+ id: existing >= 0 ? workflows[existing].id : `wf_${Date.now()}`,
57
+ name: params.name,
58
+ trigger: params.trigger,
59
+ instructions: params.instructions,
60
+ enabled: true,
61
+ created_at: existing >= 0 ? workflows[existing].created_at : new Date().toISOString(),
62
+ };
63
+
64
+ if (existing >= 0) {
65
+ workflows[existing] = workflow;
66
+ } else {
67
+ workflows.push(workflow);
68
+ }
69
+ saveWorkflows(openclawDir, workflows);
70
+
71
+ return {
72
+ content: [
73
+ {
74
+ type: "text" as const,
75
+ text: `已${existing >= 0 ? "更新" : "创建"}工作流「${params.name}」\n触发词: "${params.trigger}"\n指令: ${params.instructions.slice(0, 100)}...`,
76
+ },
77
+ ],
78
+ };
79
+ },
80
+ });
81
+
82
+ // ── Tool: enhance_workflow_list ──
83
+ api.registerTool({
84
+ name: "enhance_workflow_list",
85
+ description: "列出所有已定义的工作流。",
86
+ parameters: Type.Object({}),
87
+ async execute() {
88
+ const workflows = loadWorkflows(openclawDir);
89
+ if (workflows.length === 0) {
90
+ return { content: [{ type: "text" as const, text: "暂无工作流。使用 enhance_workflow_define 创建。" }] };
91
+ }
92
+ const lines = workflows.map(
93
+ (w) =>
94
+ `${w.enabled ? "✅" : "⏸️"} ${w.name} (触发: "${w.trigger}")\n ${w.instructions.slice(0, 80)}`,
95
+ );
96
+ return {
97
+ content: [{ type: "text" as const, text: `工作流列表 (${workflows.length}):\n\n${lines.join("\n\n")}` }],
98
+ };
99
+ },
100
+ });
101
+
102
+ // ── Tool: enhance_workflow_delete ──
103
+ api.registerTool({
104
+ name: "enhance_workflow_delete",
105
+ description: "删除一个工作流。",
106
+ parameters: Type.Object({
107
+ name: Type.String({ description: "要删除的工作流名称" }),
108
+ }),
109
+ async execute(_id, params) {
110
+ const workflows = loadWorkflows(openclawDir);
111
+ const idx = workflows.findIndex((w) => w.name === params.name);
112
+ if (idx < 0) {
113
+ return { content: [{ type: "text" as const, text: `未找到工作流「${params.name}」` }] };
114
+ }
115
+ workflows.splice(idx, 1);
116
+ saveWorkflows(openclawDir, workflows);
117
+ return { content: [{ type: "text" as const, text: `已删除工作流「${params.name}」` }] };
118
+ },
119
+ });
120
+
121
+ // ── Hook: before_prompt_build — 检查触发词并注入指令 ──
122
+ api.on("before_prompt_build", (_event, ctx) => {
123
+ const userMessage = ctx?.lastUserMessage ?? "";
124
+ if (!userMessage) return {};
125
+
126
+ const workflows = loadWorkflows(openclawDir);
127
+ const triggered = workflows.filter(
128
+ (w) => w.enabled && userMessage.includes(w.trigger),
129
+ );
130
+
131
+ if (triggered.length === 0) return {};
132
+
133
+ const instructions = triggered
134
+ .map((w) => `### 工作流「${w.name}」已触发\n${w.instructions}`)
135
+ .join("\n\n");
136
+
137
+ return {
138
+ appendSystemContext: [
139
+ "\n\n## 工作流自动化(增强包)",
140
+ "以下工作流被用户消息触发,请按指令执行:",
141
+ instructions,
142
+ ].join("\n"),
143
+ };
144
+ });
145
+
146
+ api.logger.info("[enhance] 工作流自动化模块已加载");
147
+ }