@ghenya/clinn 0.8.3 → 0.8.5
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/Src/agent.cjs +48 -4
- package/Src/index.jsx +42 -18
- package/Tools/extended_tools.js +149 -12
- package/Tools/file_tools.js +7 -1
- package/Tools/index.js +2 -0
- package/Tools/kaomoji.js +273 -0
- package/Tools/syntax_check.js +119 -0
- package/Tools/template_engine.js +105 -0
- package/bin/clinn.js +1 -1
- package/config.json +1 -0
- package/install.cjs +1 -1
- package/package.json +2 -2
package/Src/agent.cjs
CHANGED
|
@@ -22,14 +22,44 @@ function buildSystemInfo() {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function estimateTokens(text) {
|
|
25
|
-
|
|
25
|
+
if (!text) return 0;
|
|
26
26
|
let cjk = 0;
|
|
27
|
+
let ascii = 0;
|
|
27
28
|
for (const ch of text) {
|
|
28
29
|
const code = ch.codePointAt(0);
|
|
29
30
|
if (code > 127) cjk++;
|
|
30
|
-
|
|
31
|
+
else ascii++;
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
// DeepSeek tokenizer: 中文约 0.6-0.8 token/字, 英文约 0.25-0.3 token/字符
|
|
34
|
+
// 保守估计: 中文 0.8, 英文 0.3
|
|
35
|
+
return Math.ceil(cjk * 0.8 + ascii * 0.3);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 根据模型名估算上下文窗口大小
|
|
40
|
+
*/
|
|
41
|
+
function getContextWindow(model) {
|
|
42
|
+
const known = {
|
|
43
|
+
"deepseek-chat": 65536,
|
|
44
|
+
"deepseek-reasoner": 65536,
|
|
45
|
+
"deepseek-v3": 65536,
|
|
46
|
+
"deepseek-v4-pro": 131072,
|
|
47
|
+
"deepseek-r1": 65536,
|
|
48
|
+
"gpt-4": 8192,
|
|
49
|
+
"gpt-4-turbo": 128000,
|
|
50
|
+
"gpt-4o": 128000,
|
|
51
|
+
"gpt-4o-mini": 128000,
|
|
52
|
+
"gpt-3.5-turbo": 16384,
|
|
53
|
+
"claude-3-opus": 200000,
|
|
54
|
+
"claude-3-sonnet": 200000,
|
|
55
|
+
"claude-3-haiku": 200000,
|
|
56
|
+
};
|
|
57
|
+
if (!model) return 65536;
|
|
58
|
+
const key = model.toLowerCase();
|
|
59
|
+
for (const [k, v] of Object.entries(known)) {
|
|
60
|
+
if (key.includes(k)) return v;
|
|
61
|
+
}
|
|
62
|
+
return 65536;
|
|
33
63
|
}
|
|
34
64
|
|
|
35
65
|
function estimateMessagesTokens(messages) {
|
|
@@ -50,7 +80,8 @@ class Agent {
|
|
|
50
80
|
this.systemInfo = buildSystemInfo();
|
|
51
81
|
this.systemPrompt = `${config.systemPrompt}\n\n[系统环境]\n${this.systemInfo}`;
|
|
52
82
|
this.autoCompressThreshold = config.memory?.autoCompressThreshold || 5000;
|
|
53
|
-
|
|
83
|
+
// 使用模型的实际上下文窗口(从模型名推断),而不是 maxTokens(那是输出限制)
|
|
84
|
+
this.maxContextTokens = (config.llm?.contextWindow || getContextWindow(config.llm?.model)) * 0.8;
|
|
54
85
|
this._lastToolCalls = [];
|
|
55
86
|
|
|
56
87
|
Tools.setTrusted(config.tools?.trustedTools || []);
|
|
@@ -216,6 +247,8 @@ class Agent {
|
|
|
216
247
|
let allMsgs = [];
|
|
217
248
|
let warned90 = false;
|
|
218
249
|
let warned95 = false;
|
|
250
|
+
const firstIteration = this._firstRun !== false;
|
|
251
|
+
this._firstRun = false;
|
|
219
252
|
|
|
220
253
|
const activeTools = Tools.filterToolDeclarations(userMessage);
|
|
221
254
|
|
|
@@ -268,6 +301,17 @@ class Agent {
|
|
|
268
301
|
|
|
269
302
|
messages.push(stepAssistant);
|
|
270
303
|
messages.push(...toolMsgs);
|
|
304
|
+
|
|
305
|
+
// 智能压缩: 超过 8 轮工具调用后, 把旧结果截短
|
|
306
|
+
if (i > 8) {
|
|
307
|
+
for (let mi = 1; mi < messages.length - 2; mi++) {
|
|
308
|
+
const m = messages[mi];
|
|
309
|
+
if (m.role === "tool" && m.content && m.content.length > 200) {
|
|
310
|
+
m.content = m.content.slice(0, 200) + "...(已截短)";
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
271
315
|
continue;
|
|
272
316
|
}
|
|
273
317
|
|
package/Src/index.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import React, { useState, useCallback, useRef } from "react";
|
|
2
|
+
import React, { useState, useCallback, useRef, useEffect } from "react";
|
|
3
3
|
import { render, Box, Text, useInput, useStdout } from "ink";
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
5
5
|
import Spinner from "ink-spinner";
|
|
@@ -12,6 +12,7 @@ const require = createRequire(import.meta.url);
|
|
|
12
12
|
|
|
13
13
|
const Tools = require("../Tools");
|
|
14
14
|
const { listRecentTurns, searchHistory, getFileList, loadFileTurns } = require("../Mem/history");
|
|
15
|
+
const kao = require("../Tools/kaomoji");
|
|
15
16
|
|
|
16
17
|
const __filename = new URL(import.meta.url).pathname;
|
|
17
18
|
const __dirname = path.dirname(__filename);
|
|
@@ -21,7 +22,7 @@ const CLINN_CONFIG = path.join(CLINN_DIR, "config.json");
|
|
|
21
22
|
const PKG_CONFIG = path.join(__dirname, "..", "config.json");
|
|
22
23
|
const LOGO_PATH = path.join(__dirname, "..", "Logos", "StartLogo.txt");
|
|
23
24
|
|
|
24
|
-
const VER = "0.8.
|
|
25
|
+
const VER = "0.8.5";
|
|
25
26
|
|
|
26
27
|
function ensureDir() { if (!fs.existsSync(CLINN_DIR)) fs.mkdirSync(CLINN_DIR, { recursive: true }); }
|
|
27
28
|
function loadConfig() {
|
|
@@ -138,15 +139,13 @@ function Msg({ role, content }) {
|
|
|
138
139
|
system: "yellow",
|
|
139
140
|
assistant: "cyanBright",
|
|
140
141
|
};
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
assistant: "(^_^) Clinn",
|
|
145
|
-
};
|
|
142
|
+
const label = role === "user" ? "你" : role === "system" ? "系统" : "Clinn";
|
|
143
|
+
const face = content._kao || kao.matchFace(content, role);
|
|
144
|
+
const displayLabel = `${face} ${label}`;
|
|
146
145
|
const bodyColor = role === "user" ? "white" : role === "system" ? "yellow" : undefined;
|
|
147
146
|
return (
|
|
148
147
|
<Box flexDirection="column">
|
|
149
|
-
<Text color={C[role]} bold>{
|
|
148
|
+
<Text color={C[role]} bold>{displayLabel}</Text>
|
|
150
149
|
<Text color={bodyColor}>
|
|
151
150
|
{lines.map((l, i) => (i === 0 ? "" : "\n") + " " + (l || " ")).join("")}
|
|
152
151
|
</Text>
|
|
@@ -156,10 +155,11 @@ function Msg({ role, content }) {
|
|
|
156
155
|
|
|
157
156
|
function Streaming({ content }) {
|
|
158
157
|
const lines = content.split("\n");
|
|
158
|
+
const face = kao.timeBased().includes("zzz") ? "(。-ω-)zzz" : "(。-ω-)";
|
|
159
159
|
return (
|
|
160
160
|
<Box flexDirection="column">
|
|
161
161
|
<Box>
|
|
162
|
-
<Text color="cyanBright" bold>
|
|
162
|
+
<Text color="cyanBright" bold>{face} Clinn</Text>
|
|
163
163
|
<Box marginLeft={1}>
|
|
164
164
|
<Text color="cyan"><Spinner type="dots" /></Text>
|
|
165
165
|
</Box>
|
|
@@ -177,7 +177,7 @@ function ToolLog({ tools }) {
|
|
|
177
177
|
<Box key={i} flexDirection="column">
|
|
178
178
|
<Box>
|
|
179
179
|
{t.status === "done"
|
|
180
|
-
? <Text color="green"
|
|
180
|
+
? <Text color="green">✓ </Text>
|
|
181
181
|
: <Text color="yellow"><Spinner /> </Text>}
|
|
182
182
|
<Text dimColor>{t.name}</Text>
|
|
183
183
|
{t.args ? <Text color="gray"> {t.args}</Text> : null}
|
|
@@ -193,6 +193,15 @@ function ToolLog({ tools }) {
|
|
|
193
193
|
);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
function Thinking() {
|
|
197
|
+
return (
|
|
198
|
+
<Box paddingLeft={3} flexDirection="row">
|
|
199
|
+
<Text color="cyan"><Spinner type="dots" /></Text>
|
|
200
|
+
<Text color="cyan"> 正在思考...</Text>
|
|
201
|
+
</Box>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
196
205
|
let _agent = null;
|
|
197
206
|
function getAgent() {
|
|
198
207
|
if (_agent) return _agent;
|
|
@@ -244,14 +253,33 @@ function App() {
|
|
|
244
253
|
const [thinking, setThinking] = useState(false);
|
|
245
254
|
const [streaming, setStreaming] = useState("");
|
|
246
255
|
const [tools, setTools] = useState([]);
|
|
247
|
-
const [ctxPct, setCtxPct] = useState(
|
|
256
|
+
const [ctxPct, setCtxPct] = useState(0);
|
|
248
257
|
const [blocked, setBlocked] = useState(false);
|
|
249
258
|
const [slashIdx, setSlashIdx] = useState(0);
|
|
259
|
+
const [mascotFace, setMascotFace] = useState(kao.mascotForFrame());
|
|
260
|
+
const [promptFace, setPromptFace] = useState(kao.promptFace(""));
|
|
261
|
+
|
|
262
|
+
// 状态栏每 8 秒切换表情
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
const timer = setInterval(() => setMascotFace(kao.mascotForFrame()), 1000);
|
|
265
|
+
return () => clearInterval(timer);
|
|
266
|
+
}, []);
|
|
267
|
+
|
|
268
|
+
// 输入框表情: 启动动画 + 根据输入内容实时匹配
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
const timer = setInterval(() => setPromptFace(kao.promptFace(input)), 150);
|
|
271
|
+
return () => clearInterval(timer);
|
|
272
|
+
}, [input]);
|
|
250
273
|
const [msgs, setMsgs] = useState([
|
|
251
274
|
{ role: "system", content: `欢迎来到 Clinn v${VER}!Ink 全屏终端界面。` },
|
|
252
275
|
{ role: "system", content: "按 / 查看命令菜单 ↑↓选择 回车选取" },
|
|
253
276
|
]);
|
|
254
277
|
|
|
278
|
+
// 启动时从 agent 获取真实上下文占比
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
setCtxPct(getAgent().estimateContextPct());
|
|
281
|
+
}, []);
|
|
282
|
+
|
|
255
283
|
const addMsg = useCallback((role, text) => {
|
|
256
284
|
setMsgs(prev => {
|
|
257
285
|
const next = [...prev, { role, content: text }];
|
|
@@ -595,11 +623,7 @@ function App() {
|
|
|
595
623
|
<Msg key={i} role={m.role} content={m.content} />
|
|
596
624
|
))}
|
|
597
625
|
{streaming ? <Streaming content={streaming} /> : null}
|
|
598
|
-
{thinking && !streaming ?
|
|
599
|
-
<Box paddingLeft={3}>
|
|
600
|
-
<Text color="cyan"><Spinner type="dots" /> 思考中...</Text>
|
|
601
|
-
</Box>
|
|
602
|
-
) : null}
|
|
626
|
+
{thinking && !streaming ? <Thinking /> : null}
|
|
603
627
|
<ToolLog tools={tools} />
|
|
604
628
|
</Box>
|
|
605
629
|
|
|
@@ -625,7 +649,7 @@ function App() {
|
|
|
625
649
|
<Text color="gray">{"─".repeat(cols - 4)}</Text>
|
|
626
650
|
<Box paddingX={1}>
|
|
627
651
|
<Text dimColor>
|
|
628
|
-
<Text color="cyan">
|
|
652
|
+
<Text color="cyan">{mascotFace}</Text> {CONFIG.llm.model}
|
|
629
653
|
{" │ "}
|
|
630
654
|
<Text color="magenta">msg</Text> {msgs.length}
|
|
631
655
|
{" │ "}
|
|
@@ -637,7 +661,7 @@ function App() {
|
|
|
637
661
|
|
|
638
662
|
<Box paddingY={1}>
|
|
639
663
|
<Box borderStyle="round" borderColor="cyan" paddingX={1} width={cols - 4}>
|
|
640
|
-
<Text color="greenBright" bold>
|
|
664
|
+
<Text color="greenBright" bold>{promptFace} </Text>
|
|
641
665
|
<TextInput
|
|
642
666
|
value={input}
|
|
643
667
|
onChange={v => { setInput(v); setSlashIdx(0); }}
|
package/Tools/extended_tools.js
CHANGED
|
@@ -10,6 +10,7 @@ const path = require("path");
|
|
|
10
10
|
const { execSync, spawn } = require("child_process");
|
|
11
11
|
const https = require("https");
|
|
12
12
|
const http = require("http");
|
|
13
|
+
const { syntaxCheck } = require("./syntax_check");
|
|
13
14
|
|
|
14
15
|
const todoWriteTool = {
|
|
15
16
|
name: "todo_write",
|
|
@@ -60,6 +61,62 @@ const searchReplaceTool = {
|
|
|
60
61
|
},
|
|
61
62
|
};
|
|
62
63
|
|
|
64
|
+
/**
|
|
65
|
+
* 将 glob 模式转为正则表达式
|
|
66
|
+
* 支持: ** (任意深度目录), * (单层通配), ? (单字符), [abc] (字符类)
|
|
67
|
+
*/
|
|
68
|
+
function globToRegex(pattern) {
|
|
69
|
+
const parts = pattern.split("/");
|
|
70
|
+
let regex = "^";
|
|
71
|
+
for (let i = 0; i < parts.length; i++) {
|
|
72
|
+
const p = parts[i];
|
|
73
|
+
if (p === "**") {
|
|
74
|
+
if (i === parts.length - 1) {
|
|
75
|
+
// ** 在末尾: 匹配剩余所有路径
|
|
76
|
+
regex += "(?:[^/]+/)*[^/]*";
|
|
77
|
+
} else {
|
|
78
|
+
// ** 在中间: 匹配零或多级中间目录(含尾部 /)
|
|
79
|
+
regex += "(?:[^/]+/)*";
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
// 普通段: * → [^/]*, ? → [^/], 正则特殊字符转义
|
|
83
|
+
let seg = "";
|
|
84
|
+
let j = 0;
|
|
85
|
+
while (j < p.length) {
|
|
86
|
+
const ch = p[j];
|
|
87
|
+
if (ch === "*") {
|
|
88
|
+
seg += "[^/]*";
|
|
89
|
+
j++;
|
|
90
|
+
} else if (ch === "?") {
|
|
91
|
+
seg += "[^/]";
|
|
92
|
+
j++;
|
|
93
|
+
} else if (ch === "[") {
|
|
94
|
+
const end = p.indexOf("]", j);
|
|
95
|
+
if (end !== -1) {
|
|
96
|
+
seg += p.slice(j, end + 1);
|
|
97
|
+
j = end + 1;
|
|
98
|
+
} else {
|
|
99
|
+
seg += "\\[";
|
|
100
|
+
j++;
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
// 转义正则特殊字符
|
|
104
|
+
if (".+^${}()|\\".includes(ch)) seg += "\\" + ch;
|
|
105
|
+
else seg += ch;
|
|
106
|
+
j++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
regex += seg;
|
|
110
|
+
// 非末尾段, 且当前段不是 ** (它自带尾部 /)
|
|
111
|
+
if (i < parts.length - 1 && p !== "**") {
|
|
112
|
+
regex += "/";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
regex += "$";
|
|
117
|
+
return new RegExp(regex);
|
|
118
|
+
}
|
|
119
|
+
|
|
63
120
|
const globTool = {
|
|
64
121
|
name: "glob",
|
|
65
122
|
description: "文件模式匹配: 支持 **/*.js / src/**/*.ts 等glob模式, 按修改时间排序",
|
|
@@ -68,16 +125,58 @@ const globTool = {
|
|
|
68
125
|
dirPath: { type: "string", required: false, description: "搜索根目录, 默认当前目录" },
|
|
69
126
|
},
|
|
70
127
|
execute: async ({ pattern, dirPath }) => {
|
|
71
|
-
const root = dirPath || process.cwd();
|
|
128
|
+
const root = path.resolve(dirPath || process.cwd());
|
|
72
129
|
if (!fs.existsSync(root)) return `[不存在] ${root}`;
|
|
130
|
+
|
|
131
|
+
const regex = globToRegex(pattern);
|
|
132
|
+
const results = [];
|
|
133
|
+
const MAX_RESULTS = 200;
|
|
134
|
+
const IGNORE_DIRS = new Set(["node_modules", ".git", ".svn", "__pycache__", ".DS_Store"]);
|
|
135
|
+
|
|
136
|
+
function walk(dir, depth) {
|
|
137
|
+
if (depth > 20 || results.length >= MAX_RESULTS) return;
|
|
138
|
+
let entries;
|
|
139
|
+
try {
|
|
140
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
141
|
+
} catch (_) {
|
|
142
|
+
return; // 无权限目录跳过
|
|
143
|
+
}
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
if (results.length >= MAX_RESULTS) return;
|
|
146
|
+
const fullPath = path.join(dir, entry.name);
|
|
147
|
+
if (entry.isDirectory()) {
|
|
148
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
149
|
+
// 也尝试用目录路径匹配(某些 glob 只关心目录结构)
|
|
150
|
+
walk(fullPath, depth + 1);
|
|
151
|
+
} else if (entry.isFile()) {
|
|
152
|
+
const relPath = path.relative(root, fullPath);
|
|
153
|
+
// 统一用正斜杠
|
|
154
|
+
const normalized = relPath.split(path.sep).join("/");
|
|
155
|
+
if (regex.test(normalized)) {
|
|
156
|
+
try {
|
|
157
|
+
const stat = fs.statSync(fullPath);
|
|
158
|
+
results.push({ path: fullPath, mtime: stat.mtimeMs });
|
|
159
|
+
} catch (_) {
|
|
160
|
+
results.push({ path: fullPath, mtime: 0 });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
73
167
|
try {
|
|
74
|
-
|
|
75
|
-
const output = execSync(cmd, { cwd: root, encoding: "utf-8", timeout: 8000, maxBuffer: 1024 * 1024, shell: "powershell.exe" });
|
|
76
|
-
const files = output.trim().split("\r\n").filter(Boolean);
|
|
77
|
-
return files.length > 0 ? files.join("\n") : `[无匹配] ${pattern} 在 ${root}`;
|
|
168
|
+
walk(root, 1);
|
|
78
169
|
} catch (e) {
|
|
170
|
+
return `[错误] glob 遍历失败: ${e.message}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (results.length === 0) {
|
|
79
174
|
return `[无匹配] ${pattern} 在 ${root}`;
|
|
80
175
|
}
|
|
176
|
+
|
|
177
|
+
// 按修改时间降序排列
|
|
178
|
+
results.sort((a, b) => b.mtime - a.mtime);
|
|
179
|
+
return results.map((r) => r.path).join("\n");
|
|
81
180
|
},
|
|
82
181
|
};
|
|
83
182
|
|
|
@@ -140,7 +239,12 @@ const writeTool = {
|
|
|
140
239
|
const dir = path.dirname(filePath);
|
|
141
240
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
142
241
|
fs.writeFileSync(filePath, content, "utf-8");
|
|
143
|
-
|
|
242
|
+
// 语法检查
|
|
243
|
+
const err = syntaxCheck(filePath);
|
|
244
|
+
if (err) {
|
|
245
|
+
return `[OK] 已写入 ${filePath} (${content.length} 字符, ${content.split("\n").length} 行)\n[⚠ 语法警告] ${err}`;
|
|
246
|
+
}
|
|
247
|
+
return `[OK] 已写入 ${filePath} (${content.length} 字符, ${content.split("\n").length} 行) | ✓ 语法通过`;
|
|
144
248
|
},
|
|
145
249
|
};
|
|
146
250
|
|
|
@@ -352,21 +456,54 @@ const getDiagnosticsTool = {
|
|
|
352
456
|
|
|
353
457
|
const skillTool = {
|
|
354
458
|
name: "skill",
|
|
355
|
-
description: "执行一个技能模块: 从 Skills/ 目录加载预定义的技能脚本.
|
|
459
|
+
description: "执行一个技能模块: 从 Skills/ 目录加载预定义的技能脚本. 技能定义了完成特定任务的方法论和步骤, 调用后会注入上下文指导后续操作",
|
|
356
460
|
parameters: {
|
|
357
461
|
name: { type: "string", required: true, description: "技能名, 如 'code_review' / 'refactor' / 'test_gen'" },
|
|
358
462
|
},
|
|
359
463
|
execute: async ({ name }) => {
|
|
360
464
|
const skillDir = path.join(process.cwd(), "Skills");
|
|
361
|
-
if (!fs.existsSync(skillDir)) return `[无技能目录] ${skillDir} 不存在, 请创建 Skills/ 目录并放入 .js 脚本`;
|
|
362
|
-
|
|
465
|
+
if (!fs.existsSync(skillDir)) return `[无技能目录] ${skillDir} 不存在, 请创建 Skills/ 目录并放入 .cjs 或 .js 脚本`;
|
|
466
|
+
|
|
467
|
+
// 先尝试 .cjs (兼容 ESM package.json), 再尝试 .js
|
|
468
|
+
let skillPath = path.join(skillDir, name + ".cjs");
|
|
469
|
+
let ext = ".cjs";
|
|
470
|
+
if (!fs.existsSync(skillPath)) {
|
|
471
|
+
skillPath = path.join(skillDir, name + ".js");
|
|
472
|
+
ext = ".js";
|
|
473
|
+
}
|
|
363
474
|
if (!fs.existsSync(skillPath)) {
|
|
364
|
-
const available =
|
|
365
|
-
|
|
475
|
+
const available = [];
|
|
476
|
+
if (fs.existsSync(skillDir)) {
|
|
477
|
+
fs.readdirSync(skillDir)
|
|
478
|
+
.filter((f) => f.endsWith(".cjs") || f.endsWith(".js"))
|
|
479
|
+
.forEach((f) => available.push(f.replace(/\.(cjs|js)$/, "")));
|
|
480
|
+
}
|
|
481
|
+
return `[无此技能] ${name}\n可用技能: ${available.length > 0 ? [...new Set(available)].join(", ") : "(空)"}`;
|
|
366
482
|
}
|
|
367
483
|
try {
|
|
368
484
|
const mod = require(skillPath);
|
|
369
|
-
|
|
485
|
+
// 构建完整的技能上下文
|
|
486
|
+
const parts = [`[技能: ${name}]`];
|
|
487
|
+
if (mod.description) parts.push(`描述: ${mod.description}`);
|
|
488
|
+
if (mod.category) parts.push(`分类: ${mod.category}`);
|
|
489
|
+
if (mod.trigger) parts.push(`触发条件: ${mod.trigger}`);
|
|
490
|
+
if (mod.instructions) parts.push(`\n## 指令\n${mod.instructions}`);
|
|
491
|
+
if (mod.workflow && Array.isArray(mod.workflow)) {
|
|
492
|
+
parts.push(`\n## 工作流`);
|
|
493
|
+
mod.workflow.forEach((step, i) => parts.push(`${i + 1}. ${step}`));
|
|
494
|
+
}
|
|
495
|
+
if (mod.pitfalls && Array.isArray(mod.pitfalls)) {
|
|
496
|
+
parts.push(`\n## 常见陷阱`);
|
|
497
|
+
mod.pitfalls.forEach((p, i) => parts.push(`- ${p}`));
|
|
498
|
+
}
|
|
499
|
+
if (mod.examples && Array.isArray(mod.examples)) {
|
|
500
|
+
parts.push(`\n## 示例`);
|
|
501
|
+
mod.examples.forEach((ex, i) => parts.push(`### ${i + 1}.\n${ex}`));
|
|
502
|
+
}
|
|
503
|
+
// 注入必须遵循的规则
|
|
504
|
+
parts.push(`\n---`);
|
|
505
|
+
parts.push(`现在请严格按照以上技能指令执行。每个步骤都要真正执行(不描述、不跳过),完成后返回实际结果。`);
|
|
506
|
+
return parts.join("\n");
|
|
370
507
|
} catch (e) {
|
|
371
508
|
return `[技能加载失败] ${e.message}`;
|
|
372
509
|
}
|
package/Tools/file_tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { execSync } = require("child_process");
|
|
4
|
+
const { syntaxCheck } = require("./syntax_check");
|
|
4
5
|
|
|
5
6
|
const readFileTool = {
|
|
6
7
|
name: "read_file",
|
|
@@ -26,7 +27,12 @@ const writeFileTool = {
|
|
|
26
27
|
const dir = path.dirname(filePath);
|
|
27
28
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
28
29
|
fs.writeFileSync(filePath, content, "utf-8");
|
|
29
|
-
|
|
30
|
+
// 语法检查
|
|
31
|
+
const err = syntaxCheck(filePath);
|
|
32
|
+
if (err) {
|
|
33
|
+
return `[OK] 已写入 ${filePath} (${content.length} 字符)\n[⚠ 语法警告] ${err}`;
|
|
34
|
+
}
|
|
35
|
+
return `[OK] 已写入 ${filePath} (${content.length} 字符) | ✓ 语法通过`;
|
|
30
36
|
},
|
|
31
37
|
};
|
|
32
38
|
|
package/Tools/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const editTools = require("./edit_tools");
|
|
|
6
6
|
const extendedTools = require("./extended_tools");
|
|
7
7
|
const browserTools = require("./browser");
|
|
8
8
|
const tokenizer = require("./tokenizer");
|
|
9
|
+
const templateTools = require("./template_engine");
|
|
9
10
|
|
|
10
11
|
let toolRegistry = {
|
|
11
12
|
...fileTools,
|
|
@@ -13,6 +14,7 @@ let toolRegistry = {
|
|
|
13
14
|
...editTools,
|
|
14
15
|
...extendedTools,
|
|
15
16
|
...browserTools,
|
|
17
|
+
...templateTools,
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
let permissionCallback = null;
|
package/Tools/kaomoji.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 动态颜文字引擎 — 让 Clinn 的表情活起来
|
|
3
|
+
* 根据时间、状态、上下文自动切换不同的颜文字
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ========== 表情库 ==========
|
|
7
|
+
const POOLS = {
|
|
8
|
+
// 状态栏/吉祥物 — 循环,每隔几秒换一个
|
|
9
|
+
mascot: [
|
|
10
|
+
"(`・ω・´)", // 认真模式
|
|
11
|
+
"(◕‿◕✿)", // 开心
|
|
12
|
+
"( ̄▽ ̄)", // 得意
|
|
13
|
+
"(๑•̀ㅂ•́)و✧", // 干劲十足
|
|
14
|
+
"( ´_ゝ`)", // 悠闲
|
|
15
|
+
"(◍•ᴗ•◍)", // 可爱
|
|
16
|
+
"(。-ω-)", // 专注
|
|
17
|
+
"( ´∀`)", // 满足
|
|
18
|
+
"(。・ω・。)", // 乖巧
|
|
19
|
+
"(╯°□°)╯", // 燃起来了(稀有)
|
|
20
|
+
],
|
|
21
|
+
|
|
22
|
+
// 回答时 — 随机选一个
|
|
23
|
+
assistant: [
|
|
24
|
+
"(`・ω・´)", // 认真回答
|
|
25
|
+
"(๑˃̵ᴗ˂̵)و", // 兴奋
|
|
26
|
+
"(◍•ᴗ•◍)", // 可爱
|
|
27
|
+
"( ̄ω ̄)", // 轻松
|
|
28
|
+
"( ´∀`)", // 满意
|
|
29
|
+
"(・∀・)", // 活泼
|
|
30
|
+
"( ✧ω✧)", // 眼睛发光
|
|
31
|
+
"ᕦ(ò_óˇ)ᕤ", // 强壮
|
|
32
|
+
],
|
|
33
|
+
|
|
34
|
+
// 思考/流式输出时 — 专注系列
|
|
35
|
+
thinking: [
|
|
36
|
+
"(。-ω-)", // 眯眼思考
|
|
37
|
+
"(-ω- )", // 思考(左)
|
|
38
|
+
"( -ω-)", // 思考(中)
|
|
39
|
+
"(´-ω-`)", // 困倦思考
|
|
40
|
+
"(。-ˍ-。)", // 认真
|
|
41
|
+
"φ(・ω・ )", // 记笔记
|
|
42
|
+
"(ˇ_ˇ” )", // 困惑思考
|
|
43
|
+
],
|
|
44
|
+
|
|
45
|
+
// 输入框 — 随时间切换
|
|
46
|
+
prompt: [
|
|
47
|
+
"( ´ ▽ ` )ノ", // 欢快挥手
|
|
48
|
+
"(。・ω・)ノ゙", // 可爱挥手
|
|
49
|
+
"ヾ(^∇^)", // 开心
|
|
50
|
+
"( ゚∀゚)ノ", // 激动挥手
|
|
51
|
+
"(*´▽`)ノノ", // 害羞挥手
|
|
52
|
+
"ヽ(・∀・)ノ", // 兴奋
|
|
53
|
+
"(๑´ڡ`๑)ノ", // 流口水(饿了?)
|
|
54
|
+
"(´・ω・)ノ", // 温柔挥手
|
|
55
|
+
],
|
|
56
|
+
|
|
57
|
+
// 系统消息 — 各种状态
|
|
58
|
+
system: [
|
|
59
|
+
"◉‿◉", // 观察
|
|
60
|
+
"(・-・*)", // 提示
|
|
61
|
+
"✧(・∀・)✧", // 通知
|
|
62
|
+
"【・_・?】", // 疑惑
|
|
63
|
+
"(・∧・)", // 确认
|
|
64
|
+
],
|
|
65
|
+
|
|
66
|
+
// 用户 — 温暖系列
|
|
67
|
+
user: [
|
|
68
|
+
"(●'◡'●)ノ", // 可爱
|
|
69
|
+
"(。・ω・。)ノ", // 乖巧
|
|
70
|
+
"(・∀・)ノ", // 活泼
|
|
71
|
+
"(◕ᴗ◕✿)ノ", // 温柔
|
|
72
|
+
"ヽ(・∀・)ノ", // 兴奋
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// ========== 引擎 ==========
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 根据消息内容匹配颜文字
|
|
80
|
+
* @param {string} text - 消息文本
|
|
81
|
+
* @param {string} role - 角色: 'user'|'system'|'assistant'
|
|
82
|
+
* @returns {string} 匹配的颜文字
|
|
83
|
+
*/
|
|
84
|
+
function matchFace(text, role) {
|
|
85
|
+
if (!text || typeof text !== "string") return defaultForRole(role);
|
|
86
|
+
const t = text.toLowerCase();
|
|
87
|
+
|
|
88
|
+
// ====== 通用关键词 (所有角色) ======
|
|
89
|
+
// 修复/修正 (必须在失败/错误之前!)
|
|
90
|
+
if (/修复|fix|修正|修好|解决|debug/.test(t)) {
|
|
91
|
+
return "ᕙ(⇀‸↼‶)ᕗ"; // 认真干活
|
|
92
|
+
}
|
|
93
|
+
// 成功/完成
|
|
94
|
+
if (/成功|完成|好了|ok|pass|通过|done|搞定|正确|没问题/.test(t)) {
|
|
95
|
+
return "ヽ(´▽`)ノ"; // 庆祝
|
|
96
|
+
}
|
|
97
|
+
// 失败/错误/报错
|
|
98
|
+
if (/失败|错误|报错|挂了|不行|error|fail|出错|异常|不对|bug/.test(t)) {
|
|
99
|
+
return role === "assistant" ? "(´;ω;`)" : "(╥﹏╥)"; // 难过
|
|
100
|
+
}
|
|
101
|
+
// 感谢/称赞
|
|
102
|
+
if (/谢谢|感谢|厉害|好棒|nice|great|good|不错/.test(t)) {
|
|
103
|
+
return role === "user" ? "(●'◡'●)♡" : "(◍•ᴗ•◍)♡"; // 开心感动
|
|
104
|
+
}
|
|
105
|
+
// 疑问/怎么
|
|
106
|
+
if (/怎么|为什么|如何|啥|what|why|how|?|\?/.test(t) && text.length < 50) {
|
|
107
|
+
return "(・_・?)"; // 疑惑
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ====== 用户专属关键词 ======
|
|
111
|
+
if (role === "user") {
|
|
112
|
+
if (/急|快|赶紧|马上|立刻|urgent|hurry/.test(t)) return "(;・∀・)ノ"; // 着急
|
|
113
|
+
if (/搜索|搜|找|查|search|find|grep/.test(t)) return "(・ω・ )🔍"; // 搜索
|
|
114
|
+
if (/写|创建|生成|新建|create|make|build/.test(t)) return "(๑•̀ㅂ•́)و✧"; // 动手
|
|
115
|
+
if (/测试|试试|尝试|test|try/.test(t)) return "(。・ω・。)ノ"; // 尝试
|
|
116
|
+
if (/删|删掉|移除|remove|delete|rm/.test(t)) return "(´・ω・`)"; // 小心
|
|
117
|
+
if (/看|读|打开|read|cat|open|view/.test(t)) return "(◉‿◉)"; // 观察
|
|
118
|
+
return pickFrom(["user"], text);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ====== 系统专属关键词 ======
|
|
122
|
+
if (role === "system") {
|
|
123
|
+
if (/欢迎|hello|hi|启动/.test(t)) return "( ´ ▽ ` )ノ"; // 欢迎
|
|
124
|
+
if (/提示|注意|提醒|warning|warn/.test(t)) return "【・_・】"; // 提醒
|
|
125
|
+
if (/错误|失败|error|fail/.test(t)) return "✗(・∧・)"; // 警告
|
|
126
|
+
if (/成功|完成|ok|通过/.test(t)) return "✓(・∀・)"; // 确认
|
|
127
|
+
if (/帮助|help|用法/.test(t)) return "◉‿◉"; // 帮助
|
|
128
|
+
return pickFrom(["system"], text);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ====== 助手专属关键词 ======
|
|
132
|
+
if (role === "assistant") {
|
|
133
|
+
if (/搜索|search|查找|grep|find/.test(t)) return "(・ω・ )🔍"; // 搜索中
|
|
134
|
+
if (/代码|函数|文件|创建|生成|写/.test(t)) return "φ(・ω・ )"; // 码农
|
|
135
|
+
if (/分析|检查|review|审查/.test(t)) return "(`・ω・´)"; // 认真
|
|
136
|
+
if (/解释|说明|意思是/.test(t)) return "◉‿◉"; // 讲解
|
|
137
|
+
if (/安装|install|npm|pip/.test(t)) return "ᕙ(⇀‸↼‶)ᕗ"; // 干活
|
|
138
|
+
if (/测试|运行|执行|run|test/.test(t)) return "(。-ω-)"; // 观察
|
|
139
|
+
return pickFrom(["assistant"], text);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return defaultForRole(role);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 角色的默认脸
|
|
147
|
+
*/
|
|
148
|
+
function defaultForRole(role) {
|
|
149
|
+
if (role === "user") return "(●'◡'●)ノ";
|
|
150
|
+
if (role === "system") return "◉‿◉";
|
|
151
|
+
return "(`・ω・´)";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 从指定池取一个,用文本哈希做伪随机以保证同一消息不跳变
|
|
156
|
+
*/
|
|
157
|
+
function pickFrom(poolNames, seed) {
|
|
158
|
+
const all = [];
|
|
159
|
+
for (const name of poolNames) {
|
|
160
|
+
if (POOLS[name]) all.push(...POOLS[name]);
|
|
161
|
+
}
|
|
162
|
+
if (all.length === 0) return "(・ω・)";
|
|
163
|
+
// 用文本哈希做确定性选择
|
|
164
|
+
let hash = 0;
|
|
165
|
+
for (let i = 0; i < (seed || "").length; i++) {
|
|
166
|
+
hash = ((hash << 5) - hash) + seed.charCodeAt(i);
|
|
167
|
+
hash |= 0;
|
|
168
|
+
}
|
|
169
|
+
return all[Math.abs(hash) % all.length];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 根据时间返回氛围颜文字
|
|
174
|
+
*/
|
|
175
|
+
function timeBased() {
|
|
176
|
+
const h = new Date().getHours();
|
|
177
|
+
if (h < 6) return "(。-ω-)zzz"; // 深夜
|
|
178
|
+
if (h < 9) return "( ´ ▽ ` )ノ☕"; // 早晨
|
|
179
|
+
if (h < 12) return "(๑•̀ㅂ•́)و✧"; // 上午干劲
|
|
180
|
+
if (h < 14) return "(๑´ڡ`๑)"; // 午饭
|
|
181
|
+
if (h < 18) return "( ´_ゝ`)"; // 下午摸鱼
|
|
182
|
+
if (h < 21) return "ヽ(・∀・)ノ"; // 晚间活跃
|
|
183
|
+
return "(´-ω-`)"; // 夜猫子
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 状态栏更新——基于帧计数循环切换
|
|
188
|
+
*/
|
|
189
|
+
let _mascotIndex = 0;
|
|
190
|
+
let _mascotTimer = Date.now();
|
|
191
|
+
function mascotForFrame() {
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
if (now - _mascotTimer > 8000) { // 每 8 秒切换
|
|
194
|
+
_mascotTimer = now;
|
|
195
|
+
_mascotIndex = (_mascotIndex + 1) % POOLS.mascot.length;
|
|
196
|
+
}
|
|
197
|
+
return POOLS.mascot[_mascotIndex];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 启动动画序列
|
|
201
|
+
const STARTUP_SEQUENCE = [
|
|
202
|
+
{ face: "( ´ ▽ ` )ノ", ms: 0 }, // 挥手
|
|
203
|
+
{ face: "(。・ω・)ノ゙", ms: 300 }, // 用力挥手
|
|
204
|
+
{ face: "( ゚∀゚)ノ", ms: 600 }, // 激动挥手
|
|
205
|
+
{ face: "(●'◡'●)ノ", ms: 900 }, // 可爱挥手
|
|
206
|
+
{ face: "◉‿◉", ms: 1300 }, // 盯—— (稳定态)
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
let _startupStart = 0;
|
|
210
|
+
let _startupDone = false;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 输入框颜文字 — 先播启动动画,然后根据用户输入内容匹配
|
|
214
|
+
* @param {string} inputText - 当前输入框文本 (可选)
|
|
215
|
+
*/
|
|
216
|
+
function promptFace(inputText) {
|
|
217
|
+
// 启动动画
|
|
218
|
+
if (!_startupDone) {
|
|
219
|
+
if (_startupStart === 0) _startupStart = Date.now();
|
|
220
|
+
const elapsed = Date.now() - _startupStart;
|
|
221
|
+
for (let i = STARTUP_SEQUENCE.length - 1; i >= 0; i--) {
|
|
222
|
+
if (elapsed >= STARTUP_SEQUENCE[i].ms) {
|
|
223
|
+
if (i === STARTUP_SEQUENCE.length - 1) {
|
|
224
|
+
_startupDone = true; // 动画结束,切到关键字模式
|
|
225
|
+
}
|
|
226
|
+
return STARTUP_SEQUENCE[i].face;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return STARTUP_SEQUENCE[0].face;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 关键字匹配 — 根据用户在输入框里正在打的内容
|
|
233
|
+
if (!inputText || inputText.trim() === "") {
|
|
234
|
+
return "◉‿◉"; // 空输入时呆呆看着
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const t = inputText.toLowerCase();
|
|
238
|
+
|
|
239
|
+
// 斜杠命令
|
|
240
|
+
if (t.startsWith("/")) {
|
|
241
|
+
if (/help|帮助/.test(t)) return "(・ω・ )?";
|
|
242
|
+
if (/exit|退出/.test(t)) return "(´・ω・`)ノ~~";
|
|
243
|
+
if (/clear|reset/.test(t)) return "(。-ω-)";
|
|
244
|
+
if (/status|ctx/.test(t)) return "(。-ˍ-。)";
|
|
245
|
+
return "( -ω-)"; // 输入命令中
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 用户输入内容
|
|
249
|
+
if (/急|快|赶紧|马上/.test(t)) return "(;・∀・)";
|
|
250
|
+
if (/谢谢|感谢|thx/.test(t)) return "(●'◡'●)♡";
|
|
251
|
+
if (/怎么写|怎么搞|怎么弄|怎么办|求助/.test(t)) return "(´;ω;`)";
|
|
252
|
+
if (/搜|找|查|search|find|grep/.test(t)) return "(・ω・ )🔍";
|
|
253
|
+
if (/写|创建|生成|新建|create|make/.test(t)) return "(๑•̀ㅂ•́)و✧";
|
|
254
|
+
if (/改|修|fix|修复|优化|refactor/.test(t)) return "ᕙ(⇀‸↼‶)ᕗ";
|
|
255
|
+
if (/删|删掉|移除|delete|rm/.test(t)) return "(´・ω・`)";
|
|
256
|
+
if (/测|试|test|try/.test(t)) return "(。・ω・。)";
|
|
257
|
+
if (/看|读|打开|view|read|cat/.test(t)) return "◉‿◉";
|
|
258
|
+
if (/\?|?|为什么|怎么|啥|what|why/.test(t)) return "(・_・?)";
|
|
259
|
+
if (/好|ok|行|可以|yes/.test(t)) return "(◍•ᴗ•◍)";
|
|
260
|
+
if (t.length > 50) return "(`・ω・´)"; // 长篇大论——认真模式
|
|
261
|
+
|
|
262
|
+
// 默认:打字中
|
|
263
|
+
return "(。-ω-)";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
module.exports = {
|
|
267
|
+
POOLS,
|
|
268
|
+
matchFace,
|
|
269
|
+
defaultForRole,
|
|
270
|
+
mascotForFrame,
|
|
271
|
+
promptFace,
|
|
272
|
+
timeBased,
|
|
273
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 语法校验工具 — 写文件后自动检查,当场报错
|
|
3
|
+
* 支持: .js/.mjs/.cjs (node --check), .ts/.tsx (tsc), .py (py_compile), .json (JSON.parse)
|
|
4
|
+
*/
|
|
5
|
+
const { execSync } = require("child_process");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 对文件进行语法检查
|
|
11
|
+
* @param {string} filePath - 文件路径
|
|
12
|
+
* @returns {string|null} - 错误信息,null 表示通过
|
|
13
|
+
*/
|
|
14
|
+
function syntaxCheck(filePath) {
|
|
15
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
switch (ext) {
|
|
19
|
+
case ".js":
|
|
20
|
+
case ".mjs":
|
|
21
|
+
case ".cjs": {
|
|
22
|
+
execSync(`node --check ${JSON.stringify(filePath)}`, {
|
|
23
|
+
encoding: "utf-8",
|
|
24
|
+
timeout: 10000,
|
|
25
|
+
stdio: "pipe",
|
|
26
|
+
});
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
case ".ts":
|
|
31
|
+
case ".tsx": {
|
|
32
|
+
// 检查项目根是否有 tsconfig.json
|
|
33
|
+
const dir = path.dirname(filePath);
|
|
34
|
+
const tsconfig = findUp("tsconfig.json", dir);
|
|
35
|
+
if (tsconfig) {
|
|
36
|
+
execSync(`npx tsc --noEmit --pretty false 2>&1 | grep ${JSON.stringify(path.basename(filePath))} || true`, {
|
|
37
|
+
encoding: "utf-8",
|
|
38
|
+
timeout: 30000,
|
|
39
|
+
cwd: path.dirname(tsconfig),
|
|
40
|
+
stdio: "pipe",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// tsc 太慢,只做 node 基础检查
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
case ".py": {
|
|
48
|
+
execSync(`python3 -m py_compile ${JSON.stringify(filePath)}`, {
|
|
49
|
+
encoding: "utf-8",
|
|
50
|
+
timeout: 10000,
|
|
51
|
+
stdio: "pipe",
|
|
52
|
+
});
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case ".json": {
|
|
57
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
58
|
+
JSON.parse(raw);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case ".html":
|
|
63
|
+
case ".css":
|
|
64
|
+
case ".scss":
|
|
65
|
+
case ".md":
|
|
66
|
+
case ".txt":
|
|
67
|
+
case ".yaml":
|
|
68
|
+
case ".yml":
|
|
69
|
+
case ".toml":
|
|
70
|
+
// 这些格式不做语法检查
|
|
71
|
+
return null;
|
|
72
|
+
|
|
73
|
+
default:
|
|
74
|
+
// 未知扩展名,尝试 node --check(可能是无扩展名的 JS)
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// 提取有用的错误信息
|
|
79
|
+
let msg = (e.stderr || e.stdout || e.message || "").toString();
|
|
80
|
+
|
|
81
|
+
// 精简 tsc 输出
|
|
82
|
+
if (ext === ".ts" || ext === ".tsx") {
|
|
83
|
+
const lines = msg.split("\n").filter((l) => l.includes(path.basename(filePath)));
|
|
84
|
+
if (lines.length > 0) {
|
|
85
|
+
msg = lines.slice(0, 3).join("\n");
|
|
86
|
+
} else {
|
|
87
|
+
msg = msg.slice(0, 300);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 精简 node 输出
|
|
92
|
+
if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
|
|
93
|
+
msg = msg.split("\n").slice(0, 3).join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 精简 python 输出
|
|
97
|
+
if (ext === ".py") {
|
|
98
|
+
msg = msg.split("\n").filter((l) => l.trim()).slice(0, 3).join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return msg.slice(0, 500);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 向上查找文件
|
|
107
|
+
*/
|
|
108
|
+
function findUp(filename, startDir) {
|
|
109
|
+
let dir = path.resolve(startDir);
|
|
110
|
+
const root = path.parse(dir).root;
|
|
111
|
+
while (true) {
|
|
112
|
+
const p = path.join(dir, filename);
|
|
113
|
+
if (fs.existsSync(p)) return p;
|
|
114
|
+
if (dir === root) return null;
|
|
115
|
+
dir = path.dirname(dir);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { syntaxCheck, findUp };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模版引擎 — 类似 Trae 的代码骨架系统
|
|
3
|
+
* 从 Skills/templates/ 加载预置模版,生成带语法检查的代码骨架
|
|
4
|
+
*/
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { syntaxCheck } = require("./syntax_check");
|
|
8
|
+
|
|
9
|
+
const TEMPLATES_DIR = path.join(__dirname, "..", "Skills", "templates");
|
|
10
|
+
|
|
11
|
+
function loadTemplates() {
|
|
12
|
+
if (!fs.existsSync(TEMPLATES_DIR)) return {};
|
|
13
|
+
const files = fs.readdirSync(TEMPLATES_DIR).filter((f) => f.endsWith(".cjs"));
|
|
14
|
+
const templates = {};
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
try {
|
|
17
|
+
const mod = require(path.join(TEMPLATES_DIR, file));
|
|
18
|
+
const name = file.replace(/\.cjs$/, "");
|
|
19
|
+
templates[name] = {
|
|
20
|
+
name,
|
|
21
|
+
description: mod.description || "(无描述)",
|
|
22
|
+
category: mod.category || "general",
|
|
23
|
+
params: mod.params || {},
|
|
24
|
+
generate: mod.generate,
|
|
25
|
+
};
|
|
26
|
+
} catch (_) {}
|
|
27
|
+
}
|
|
28
|
+
return templates;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const useTemplateTool = {
|
|
32
|
+
name: "use_template",
|
|
33
|
+
description: "代码模版引擎: 列出可用模版 或 按模版生成代码骨架. 类似 Trae 的模板系统",
|
|
34
|
+
parameters: {
|
|
35
|
+
action: { type: "string", required: true, description: "list(列出所有模版) 或 generate(按模版名生成代码)" },
|
|
36
|
+
name: { type: "string", required: false, description: "模版名, generate 时必填, 如 'express-api' 或 'react-component'" },
|
|
37
|
+
params: { type: "string", required: false, description: "模版参数, JSON 字符串, 如 '{\"name\":\"myApp\"}'" },
|
|
38
|
+
},
|
|
39
|
+
execute: async ({ action, name, params }) => {
|
|
40
|
+
const templates = loadTemplates();
|
|
41
|
+
|
|
42
|
+
if (action === "list") {
|
|
43
|
+
const names = Object.keys(templates);
|
|
44
|
+
if (names.length === 0) return "(暂无可用模版)\n提示: 将模版文件放入 Skills/templates/ 目录";
|
|
45
|
+
|
|
46
|
+
// 按分类分组
|
|
47
|
+
const groups = {};
|
|
48
|
+
for (const [key, t] of Object.entries(templates)) {
|
|
49
|
+
if (!groups[t.category]) groups[t.category] = [];
|
|
50
|
+
groups[t.category].push(t);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const lines = [`可用模版 (${names.length} 个):`];
|
|
54
|
+
for (const [cat, items] of Object.entries(groups)) {
|
|
55
|
+
lines.push(`\n[${cat}]`);
|
|
56
|
+
for (const t of items) {
|
|
57
|
+
lines.push(` ${t.name} — ${t.description}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
lines.push(`\n用法: use_template action=generate name=<模版名>`);
|
|
61
|
+
return lines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (action === "generate") {
|
|
65
|
+
if (!name) return "[失败] 请指定模版名, 用 action=list 查看可用模版";
|
|
66
|
+
|
|
67
|
+
const template = templates[name];
|
|
68
|
+
if (!template) {
|
|
69
|
+
const available = Object.keys(templates).join(", ");
|
|
70
|
+
return `[不存在] 模版 "${name}"\n可用: ${available || "(无)"}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let parsedParams = {};
|
|
74
|
+
if (params) {
|
|
75
|
+
try {
|
|
76
|
+
parsedParams = JSON.parse(params);
|
|
77
|
+
} catch (_) {
|
|
78
|
+
return `[失败] params 不是合法 JSON: ${params}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const code = template.generate(parsedParams);
|
|
84
|
+
return [
|
|
85
|
+
`[模版: ${name}] ${template.description}`,
|
|
86
|
+
`参数: ${JSON.stringify(parsedParams)}`,
|
|
87
|
+
"",
|
|
88
|
+
"```",
|
|
89
|
+
code,
|
|
90
|
+
"```",
|
|
91
|
+
"",
|
|
92
|
+
"提示: 用 write_file 将以上代码写入文件",
|
|
93
|
+
].join("\n");
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return `[模版生成失败] ${name}: ${e.message}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return `[错误] 未知 action: ${action}, 可用: list / generate`;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
use_template: useTemplateTool,
|
|
105
|
+
};
|
package/bin/clinn.js
CHANGED
|
@@ -4,7 +4,7 @@ const path = require("path");
|
|
|
4
4
|
const { pathToFileURL } = require("url");
|
|
5
5
|
|
|
6
6
|
const tsxPath = pathToFileURL(path.join(__dirname, "..", "node_modules", "tsx", "dist", "esm", "index.mjs")).href;
|
|
7
|
-
const srcIndex =
|
|
7
|
+
const srcIndex = path.join(__dirname, "..", "Src", "index.jsx");
|
|
8
8
|
|
|
9
9
|
const result = spawnSync(process.execPath, [
|
|
10
10
|
"--import", tsxPath,
|
package/config.json
CHANGED
package/install.cjs
CHANGED
|
@@ -6,7 +6,7 @@ const fs = require("fs");
|
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const { execSync } = require("child_process");
|
|
8
8
|
|
|
9
|
-
const VER = "0.8.
|
|
9
|
+
const VER = "0.8.4";
|
|
10
10
|
const G = "\x1b[0;32m", C = "\x1b[0;36m", Y = "\x1b[0;33m", R = "\x1b[0;31m", N = "\x1b[0m", D = "\x1b[2m";
|
|
11
11
|
|
|
12
12
|
const IS_WIN = process.platform === "win32";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghenya/clinn",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "终端原生 AI 编程助手 — DeepSeek 驱动,50+ 工具,对话记忆,虚拟浏览器,Ink 全屏界面",
|
|
5
5
|
"main": "Src/index.jsx",
|
|
6
6
|
"bin": {
|
|
@@ -58,4 +58,4 @@
|
|
|
58
58
|
"react": "^18.3.1",
|
|
59
59
|
"tsx": "^4.0.0"
|
|
60
60
|
}
|
|
61
|
-
}
|
|
61
|
+
}
|