@ghenya/clinn 0.8.4 → 0.8.6

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/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.4";
25
+ const VER = "0.8.6";
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 L = {
142
- user: "(^o^)ノ 你",
143
- system: "(!_!) 系统",
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>{L[role]}</Text>
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>(^_^) Clinn</Text>
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">(^_^)b </Text>
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(15);
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">(ΦωΦ)</Text> {CONFIG.llm.model}
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>("・ω・)ノ </Text>
664
+ <Text color="greenBright" bold>{promptFace} </Text>
641
665
  <TextInput
642
666
  value={input}
643
667
  onChange={v => { setInput(v); setSlashIdx(0); }}
@@ -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
- const cmd = `Get-ChildItem -Path "${root}" -Filter "${pattern.split("/").pop()}" -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.FullName -notmatch 'node_modules|\\\\\.git\\\\' } | Select-Object -First 100 | ForEach-Object { $_.FullName }`;
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
- return `[OK] 已写入 ${filePath} (${content.length} 字符, ${content.split("\n").length} 行)`;
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
- const skillPath = path.join(skillDir, name + ".js");
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 = fs.readdirSync(skillDir).filter((f) => f.endsWith(".js")).map((f) => f.replace(".js", ""));
365
- return `[无此技能] ${name}\n可用技能: ${available.length > 0 ? available.join(", ") : "(空)"}`;
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
- return `[技能: ${name}]\n${mod.description || "(无描述)"}\n指令: ${mod.instructions || "(无指令)"}`;
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
  }
@@ -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
- return `[OK] 已写入 ${filePath} (${content.length} 字符)`;
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;
@@ -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
+ };