chainlesschain 0.38.1 → 0.40.1
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/package.json +3 -2
- package/src/commands/agent.js +5 -1
- package/src/commands/ask.js +24 -9
- package/src/commands/chat.js +5 -1
- package/src/commands/cli-anything.js +266 -0
- package/src/commands/serve.js +109 -0
- package/src/constants.js +1 -0
- package/src/index.js +12 -0
- package/src/lib/cli-anything-bridge.js +379 -0
- package/src/lib/git-integration.js +1 -1
- package/src/lib/llm-providers.js +14 -1
- package/src/lib/task-model-selector.js +232 -0
- package/src/lib/ws-server.js +474 -0
- package/src/repl/agent-repl.js +25 -4
- package/src/repl/chat-repl.js +14 -6
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-Anything Bridge — discovers and registers CLI-Anything generated tools
|
|
3
|
+
* as ChainlessChain managed-layer skills.
|
|
4
|
+
*
|
|
5
|
+
* CLI-Anything (https://github.com/HKUDS/CLI-Anything) generates Agent-native
|
|
6
|
+
* CLI wrappers for arbitrary software. This bridge scans for those wrappers
|
|
7
|
+
* on the user's PATH and turns each one into a SKILL.md + handler.js pair
|
|
8
|
+
* that the existing 4-layer skill-loader picks up automatically.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { getElectronUserDataDir } from "./paths.js";
|
|
15
|
+
|
|
16
|
+
/* ---------- _deps injection (Vitest CJS mock pattern) ---------- */
|
|
17
|
+
export const _deps = { execSync, fs, path };
|
|
18
|
+
|
|
19
|
+
/* ----------------------------------------------------------------
|
|
20
|
+
* Database helpers
|
|
21
|
+
* ---------------------------------------------------------------- */
|
|
22
|
+
|
|
23
|
+
const CREATE_TABLE_SQL = `
|
|
24
|
+
CREATE TABLE IF NOT EXISTS cli_anything_tools (
|
|
25
|
+
id TEXT PRIMARY KEY,
|
|
26
|
+
name TEXT NOT NULL UNIQUE,
|
|
27
|
+
software_path TEXT,
|
|
28
|
+
cli_command TEXT NOT NULL,
|
|
29
|
+
version TEXT DEFAULT '1.0.0',
|
|
30
|
+
description TEXT,
|
|
31
|
+
subcommands TEXT,
|
|
32
|
+
skill_name TEXT,
|
|
33
|
+
status TEXT DEFAULT 'discovered',
|
|
34
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
35
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
36
|
+
)`;
|
|
37
|
+
|
|
38
|
+
export function ensureCliAnythingTables(db) {
|
|
39
|
+
db.exec(CREATE_TABLE_SQL);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ----------------------------------------------------------------
|
|
43
|
+
* Python / CLI-Anything detection
|
|
44
|
+
* ---------------------------------------------------------------- */
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Detect a usable Python interpreter.
|
|
48
|
+
* Returns { found: boolean, command?: string, version?: string }.
|
|
49
|
+
*/
|
|
50
|
+
export function detectPython() {
|
|
51
|
+
const candidates =
|
|
52
|
+
process.platform === "win32"
|
|
53
|
+
? ["python", "python3", "py"]
|
|
54
|
+
: ["python3", "python"];
|
|
55
|
+
|
|
56
|
+
for (const cmd of candidates) {
|
|
57
|
+
try {
|
|
58
|
+
const ver = _deps
|
|
59
|
+
.execSync(`${cmd} --version`, {
|
|
60
|
+
encoding: "utf-8",
|
|
61
|
+
timeout: 10000,
|
|
62
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
63
|
+
})
|
|
64
|
+
.trim();
|
|
65
|
+
const match = ver.match(/Python\s+([\d.]+)/i);
|
|
66
|
+
if (match) {
|
|
67
|
+
return { found: true, command: cmd, version: match[1] };
|
|
68
|
+
}
|
|
69
|
+
} catch (_err) {
|
|
70
|
+
// This candidate not available — try next
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { found: false };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check whether the `cli-anything` Python package is installed.
|
|
78
|
+
* Returns { installed: boolean, version?: string }.
|
|
79
|
+
*/
|
|
80
|
+
export function detectCliAnything() {
|
|
81
|
+
const py = detectPython();
|
|
82
|
+
if (!py.found) return { installed: false };
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const out = _deps.execSync(`${py.command} -m pip show cli-anything`, {
|
|
86
|
+
encoding: "utf-8",
|
|
87
|
+
timeout: 15000,
|
|
88
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
89
|
+
});
|
|
90
|
+
const verMatch = out.match(/Version:\s*([\d.]+)/i);
|
|
91
|
+
return {
|
|
92
|
+
installed: true,
|
|
93
|
+
version: verMatch ? verMatch[1] : "unknown",
|
|
94
|
+
pythonCommand: py.command,
|
|
95
|
+
};
|
|
96
|
+
} catch (_err) {
|
|
97
|
+
return { installed: false, pythonCommand: py.command };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Install CLI-Anything via pip.
|
|
103
|
+
*/
|
|
104
|
+
export function installCliAnything(pythonCmd) {
|
|
105
|
+
_deps.execSync(`${pythonCmd} -m pip install cli-anything`, {
|
|
106
|
+
encoding: "utf-8",
|
|
107
|
+
timeout: 120000,
|
|
108
|
+
stdio: "inherit",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* ----------------------------------------------------------------
|
|
113
|
+
* Tool scanning
|
|
114
|
+
* ---------------------------------------------------------------- */
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Scan PATH for executables matching `cli-anything-*`.
|
|
118
|
+
* Returns an array of { name, command, path }.
|
|
119
|
+
*/
|
|
120
|
+
export function scanPathForTools() {
|
|
121
|
+
const results = [];
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
const dirs = (process.env.PATH || "").split(_deps.path.delimiter);
|
|
124
|
+
|
|
125
|
+
for (const dir of dirs) {
|
|
126
|
+
try {
|
|
127
|
+
const entries = _deps.fs.readdirSync(dir);
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
const baseName = entry.replace(/\.exe$/i, "");
|
|
130
|
+
if (!baseName.startsWith("cli-anything-")) continue;
|
|
131
|
+
if (seen.has(baseName)) continue;
|
|
132
|
+
seen.add(baseName);
|
|
133
|
+
|
|
134
|
+
const toolName = baseName.replace(/^cli-anything-/, "");
|
|
135
|
+
results.push({
|
|
136
|
+
name: toolName,
|
|
137
|
+
command: baseName,
|
|
138
|
+
path: _deps.path.join(dir, entry),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
} catch (_err) {
|
|
142
|
+
// Directory not readable — skip
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return results;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Parse `--help` output of a cli-anything generated tool.
|
|
150
|
+
* Returns { description, subcommands: [{ name, description }] }.
|
|
151
|
+
*/
|
|
152
|
+
export function parseToolHelp(command) {
|
|
153
|
+
let helpText;
|
|
154
|
+
try {
|
|
155
|
+
helpText = _deps.execSync(`${command} --help`, {
|
|
156
|
+
encoding: "utf-8",
|
|
157
|
+
timeout: 15000,
|
|
158
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
159
|
+
});
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const raw = err.stdout || err.stderr || "";
|
|
162
|
+
helpText = typeof raw === "string" ? raw : raw.toString("utf-8");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const lines = helpText.split("\n").map((l) => l.trimEnd());
|
|
166
|
+
|
|
167
|
+
// First non-empty, non-"usage:" line is typically the description
|
|
168
|
+
let description = "";
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
const trimmed = line.trim();
|
|
171
|
+
if (!trimmed) continue;
|
|
172
|
+
if (/^usage:/i.test(trimmed)) continue;
|
|
173
|
+
if (/^options?:/i.test(trimmed)) break;
|
|
174
|
+
if (/^commands?:/i.test(trimmed)) break;
|
|
175
|
+
description = trimmed;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Parse subcommands section
|
|
180
|
+
const subcommands = [];
|
|
181
|
+
let inCommands = false;
|
|
182
|
+
for (const line of lines) {
|
|
183
|
+
const trimmed = line.trim();
|
|
184
|
+
if (/^commands?:/i.test(trimmed) || /^subcommands?:/i.test(trimmed)) {
|
|
185
|
+
inCommands = true;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (inCommands) {
|
|
189
|
+
if (!trimmed || /^options?:/i.test(trimmed)) break;
|
|
190
|
+
const match = trimmed.match(/^(\S+)\s+(.*)/);
|
|
191
|
+
if (match) {
|
|
192
|
+
subcommands.push({ name: match[1], description: match[2].trim() });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
description: description || `CLI-Anything tool: ${command}`,
|
|
199
|
+
subcommands,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* ----------------------------------------------------------------
|
|
204
|
+
* Skill registration / removal
|
|
205
|
+
* ---------------------------------------------------------------- */
|
|
206
|
+
|
|
207
|
+
function _skillDir(toolName) {
|
|
208
|
+
const userData = getElectronUserDataDir();
|
|
209
|
+
return _deps.path.join(userData, "skills", `cli-anything-${toolName}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Generate SKILL.md content for a CLI-Anything tool.
|
|
214
|
+
*/
|
|
215
|
+
export function _generateSkillMd(name, helpData) {
|
|
216
|
+
const subs = (helpData.subcommands || [])
|
|
217
|
+
.map((s) => `- **${s.name}**: ${s.description}`)
|
|
218
|
+
.join("\n");
|
|
219
|
+
|
|
220
|
+
return `---
|
|
221
|
+
name: cli-anything-${name}
|
|
222
|
+
display-name: CLI-Anything ${name}
|
|
223
|
+
description: ${helpData.description || `Agent-native CLI for ${name}`}
|
|
224
|
+
version: 1.0.0
|
|
225
|
+
category: integration
|
|
226
|
+
tags: [cli-anything, ${name}, external-tool]
|
|
227
|
+
user-invocable: true
|
|
228
|
+
handler: handler.js
|
|
229
|
+
capabilities: [shell-exec]
|
|
230
|
+
os: [linux, darwin, win32]
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
# cli-anything-${name}
|
|
234
|
+
|
|
235
|
+
Auto-registered by \`chainlesschain cli-anything register ${name}\`.
|
|
236
|
+
|
|
237
|
+
${helpData.description || ""}
|
|
238
|
+
|
|
239
|
+
${subs ? `## Subcommands\n\n${subs}\n` : ""}
|
|
240
|
+
## Usage
|
|
241
|
+
|
|
242
|
+
Pass the subcommand and arguments as plain text input:
|
|
243
|
+
|
|
244
|
+
\`\`\`
|
|
245
|
+
/skill cli-anything-${name} <subcommand> [args...]
|
|
246
|
+
\`\`\`
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Generate handler.js content for a CLI-Anything tool.
|
|
252
|
+
*/
|
|
253
|
+
export function _generateHandlerJs(name, command) {
|
|
254
|
+
return `"use strict";
|
|
255
|
+
const { execSync } = require("child_process");
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
async execute(task, context) {
|
|
259
|
+
const input = (task?.params?.input || task?.action || "").trim();
|
|
260
|
+
if (!input) return { success: false, error: "No input provided" };
|
|
261
|
+
try {
|
|
262
|
+
const output = execSync(\`${command} \${input}\`, {
|
|
263
|
+
encoding: "utf-8",
|
|
264
|
+
timeout: 60000,
|
|
265
|
+
cwd: context?.projectRoot || process.cwd(),
|
|
266
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
267
|
+
});
|
|
268
|
+
let result;
|
|
269
|
+
try { result = JSON.parse(output); } catch (_e) { result = { output: output.trim() }; }
|
|
270
|
+
return { success: true, result, message: "Completed" };
|
|
271
|
+
} catch (err) {
|
|
272
|
+
const errMsg = typeof err.stderr === "string" ? err.stderr : (err.stderr?.toString("utf-8") || "");
|
|
273
|
+
return { success: false, error: errMsg || err.message };
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Register a CLI-Anything tool as a managed-layer skill.
|
|
282
|
+
*/
|
|
283
|
+
export function registerTool(db, toolName, opts = {}) {
|
|
284
|
+
const command = opts.command || `cli-anything-${toolName}`;
|
|
285
|
+
const helpData = opts.helpData || parseToolHelp(command);
|
|
286
|
+
const force = opts.force || false;
|
|
287
|
+
|
|
288
|
+
const dir = _skillDir(toolName);
|
|
289
|
+
|
|
290
|
+
// Check existing
|
|
291
|
+
const existing = db
|
|
292
|
+
.prepare("SELECT id FROM cli_anything_tools WHERE name = ?")
|
|
293
|
+
.get(toolName);
|
|
294
|
+
if (existing && !force) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`Tool "${toolName}" already registered. Use --force to overwrite.`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Write skill files
|
|
301
|
+
_deps.fs.mkdirSync(dir, { recursive: true });
|
|
302
|
+
_deps.fs.writeFileSync(
|
|
303
|
+
_deps.path.join(dir, "SKILL.md"),
|
|
304
|
+
_generateSkillMd(toolName, helpData),
|
|
305
|
+
"utf-8",
|
|
306
|
+
);
|
|
307
|
+
_deps.fs.writeFileSync(
|
|
308
|
+
_deps.path.join(dir, "handler.js"),
|
|
309
|
+
_generateHandlerJs(toolName, command),
|
|
310
|
+
"utf-8",
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// Upsert DB record
|
|
314
|
+
const id = existing ? existing.id : `clia-${toolName}-${Date.now()}`;
|
|
315
|
+
const skillName = `cli-anything-${toolName}`;
|
|
316
|
+
const subcommandsJson = JSON.stringify(helpData.subcommands || []);
|
|
317
|
+
|
|
318
|
+
if (existing) {
|
|
319
|
+
db.prepare(
|
|
320
|
+
`
|
|
321
|
+
UPDATE cli_anything_tools
|
|
322
|
+
SET cli_command = ?, description = ?, subcommands = ?,
|
|
323
|
+
skill_name = ?, status = 'registered', updated_at = datetime('now')
|
|
324
|
+
WHERE name = ?
|
|
325
|
+
`,
|
|
326
|
+
).run(command, helpData.description, subcommandsJson, skillName, toolName);
|
|
327
|
+
} else {
|
|
328
|
+
db.prepare(
|
|
329
|
+
`
|
|
330
|
+
INSERT INTO cli_anything_tools (id, name, software_path, cli_command, description, subcommands, skill_name, status)
|
|
331
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'registered')
|
|
332
|
+
`,
|
|
333
|
+
).run(
|
|
334
|
+
id,
|
|
335
|
+
toolName,
|
|
336
|
+
opts.softwarePath || null,
|
|
337
|
+
command,
|
|
338
|
+
helpData.description,
|
|
339
|
+
subcommandsJson,
|
|
340
|
+
skillName,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { id, skillName, dir, subcommands: helpData.subcommands || [] };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Remove a registered CLI-Anything tool.
|
|
349
|
+
*/
|
|
350
|
+
export function removeTool(db, toolName) {
|
|
351
|
+
const row = db
|
|
352
|
+
.prepare("SELECT id FROM cli_anything_tools WHERE name = ?")
|
|
353
|
+
.get(toolName);
|
|
354
|
+
if (!row) {
|
|
355
|
+
throw new Error(`Tool "${toolName}" is not registered.`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Remove skill directory
|
|
359
|
+
const dir = _skillDir(toolName);
|
|
360
|
+
try {
|
|
361
|
+
_deps.fs.rmSync(dir, { recursive: true, force: true });
|
|
362
|
+
} catch (_err) {
|
|
363
|
+
// Directory may already be gone
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
db.prepare("DELETE FROM cli_anything_tools WHERE name = ?").run(toolName);
|
|
367
|
+
return { removed: true, toolName };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* List all registered CLI-Anything tools.
|
|
372
|
+
*/
|
|
373
|
+
export function listTools(db) {
|
|
374
|
+
return db
|
|
375
|
+
.prepare(
|
|
376
|
+
"SELECT id, name, cli_command, description, skill_name, status, created_at, updated_at FROM cli_anything_tools ORDER BY name",
|
|
377
|
+
)
|
|
378
|
+
.all();
|
|
379
|
+
}
|
|
@@ -24,7 +24,7 @@ export function gitExec(args, cwd) {
|
|
|
24
24
|
stdio: ["pipe", "pipe", "pipe"],
|
|
25
25
|
}).trim();
|
|
26
26
|
} catch (err) {
|
|
27
|
-
const stderr = err.stderr ? err.stderr.toString().trim() : "";
|
|
27
|
+
const stderr = err.stderr ? err.stderr.toString("utf-8").trim() : "";
|
|
28
28
|
throw new Error(stderr || err.message);
|
|
29
29
|
}
|
|
30
30
|
}
|
package/src/lib/llm-providers.js
CHANGED
|
@@ -70,6 +70,19 @@ export const BUILT_IN_PROVIDERS = {
|
|
|
70
70
|
],
|
|
71
71
|
free: false,
|
|
72
72
|
},
|
|
73
|
+
volcengine: {
|
|
74
|
+
name: "volcengine",
|
|
75
|
+
displayName: "Volcengine (豆包)",
|
|
76
|
+
baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
|
|
77
|
+
apiKeyEnv: "VOLCENGINE_API_KEY",
|
|
78
|
+
models: [
|
|
79
|
+
"doubao-seed-1-6-251015",
|
|
80
|
+
"doubao-seed-1-6-flash-250828",
|
|
81
|
+
"doubao-seed-1-6-lite-251015",
|
|
82
|
+
"doubao-seed-code",
|
|
83
|
+
],
|
|
84
|
+
free: false,
|
|
85
|
+
},
|
|
73
86
|
};
|
|
74
87
|
|
|
75
88
|
/**
|
|
@@ -302,7 +315,7 @@ export class LLMProviderRegistry {
|
|
|
302
315
|
return { ok: true, elapsed: Date.now() - start, response: text.trim() };
|
|
303
316
|
}
|
|
304
317
|
|
|
305
|
-
// OpenAI-compatible (openai, deepseek, dashscope, mistral)
|
|
318
|
+
// OpenAI-compatible (openai, deepseek, dashscope, mistral, volcengine)
|
|
306
319
|
const key = this.getApiKey(name);
|
|
307
320
|
if (!key) throw new Error(`${provider.apiKeyEnv} not set`);
|
|
308
321
|
const res = await fetch(`${provider.baseUrl}/chat/completions`, {
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task-based intelligent model selector for CLI
|
|
3
|
+
*
|
|
4
|
+
* Detects task type from user messages and recommends the best model
|
|
5
|
+
* for each LLM provider. Enables automatic model switching based on
|
|
6
|
+
* what the user is trying to accomplish.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Task types supported by the selector
|
|
11
|
+
*/
|
|
12
|
+
export const TaskType = {
|
|
13
|
+
CHAT: "chat",
|
|
14
|
+
CODE: "code",
|
|
15
|
+
REASONING: "reasoning",
|
|
16
|
+
FAST: "fast",
|
|
17
|
+
TRANSLATE: "translate",
|
|
18
|
+
CREATIVE: "creative",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Task type → recommended model per provider
|
|
23
|
+
* Each provider maps to the best model for that task type.
|
|
24
|
+
*/
|
|
25
|
+
const TASK_MODEL_MAP = {
|
|
26
|
+
[TaskType.CHAT]: {
|
|
27
|
+
volcengine: "doubao-seed-1-6-flash-250828",
|
|
28
|
+
openai: "gpt-4o-mini",
|
|
29
|
+
anthropic: "claude-sonnet-4-6",
|
|
30
|
+
deepseek: "deepseek-chat",
|
|
31
|
+
dashscope: "qwen-plus",
|
|
32
|
+
gemini: "gemini-2.0-flash",
|
|
33
|
+
mistral: "mistral-medium-latest",
|
|
34
|
+
ollama: "qwen2:7b",
|
|
35
|
+
},
|
|
36
|
+
[TaskType.CODE]: {
|
|
37
|
+
volcengine: "doubao-seed-code",
|
|
38
|
+
openai: "gpt-4o",
|
|
39
|
+
anthropic: "claude-sonnet-4-6",
|
|
40
|
+
deepseek: "deepseek-coder",
|
|
41
|
+
dashscope: "qwen-max",
|
|
42
|
+
gemini: "gemini-2.0-pro",
|
|
43
|
+
mistral: "mistral-large-latest",
|
|
44
|
+
ollama: "codellama:7b",
|
|
45
|
+
},
|
|
46
|
+
[TaskType.REASONING]: {
|
|
47
|
+
volcengine: "doubao-seed-1-6-251015",
|
|
48
|
+
openai: "o1",
|
|
49
|
+
anthropic: "claude-opus-4-6",
|
|
50
|
+
deepseek: "deepseek-reasoner",
|
|
51
|
+
dashscope: "qwen-max",
|
|
52
|
+
gemini: "gemini-2.0-pro",
|
|
53
|
+
mistral: "mistral-large-latest",
|
|
54
|
+
ollama: "qwen2:7b",
|
|
55
|
+
},
|
|
56
|
+
[TaskType.FAST]: {
|
|
57
|
+
volcengine: "doubao-seed-1-6-lite-251015",
|
|
58
|
+
openai: "gpt-4o-mini",
|
|
59
|
+
anthropic: "claude-haiku-4-5-20251001",
|
|
60
|
+
deepseek: "deepseek-chat",
|
|
61
|
+
dashscope: "qwen-turbo",
|
|
62
|
+
gemini: "gemini-2.0-flash",
|
|
63
|
+
mistral: "mistral-small-latest",
|
|
64
|
+
ollama: "qwen2:7b",
|
|
65
|
+
},
|
|
66
|
+
[TaskType.TRANSLATE]: {
|
|
67
|
+
volcengine: "doubao-seed-1-6-251015",
|
|
68
|
+
openai: "gpt-4o",
|
|
69
|
+
anthropic: "claude-sonnet-4-6",
|
|
70
|
+
deepseek: "deepseek-chat",
|
|
71
|
+
dashscope: "qwen-plus",
|
|
72
|
+
gemini: "gemini-2.0-flash",
|
|
73
|
+
mistral: "mistral-large-latest",
|
|
74
|
+
ollama: "qwen2:7b",
|
|
75
|
+
},
|
|
76
|
+
[TaskType.CREATIVE]: {
|
|
77
|
+
volcengine: "doubao-seed-1-6-251015",
|
|
78
|
+
openai: "gpt-4o",
|
|
79
|
+
anthropic: "claude-opus-4-6",
|
|
80
|
+
deepseek: "deepseek-chat",
|
|
81
|
+
dashscope: "qwen-max",
|
|
82
|
+
gemini: "gemini-2.0-pro",
|
|
83
|
+
mistral: "mistral-large-latest",
|
|
84
|
+
ollama: "qwen2:7b",
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Task type display names (Chinese + English)
|
|
90
|
+
*/
|
|
91
|
+
const TASK_NAMES = {
|
|
92
|
+
[TaskType.CHAT]: "日常对话",
|
|
93
|
+
[TaskType.CODE]: "代码任务",
|
|
94
|
+
[TaskType.REASONING]: "复杂推理",
|
|
95
|
+
[TaskType.FAST]: "快速响应",
|
|
96
|
+
[TaskType.TRANSLATE]: "翻译任务",
|
|
97
|
+
[TaskType.CREATIVE]: "创意写作",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Keyword patterns for detecting task type from user message.
|
|
102
|
+
* Each pattern is [regex, taskType, priority].
|
|
103
|
+
* Higher priority wins when multiple patterns match.
|
|
104
|
+
*/
|
|
105
|
+
const TASK_PATTERNS = [
|
|
106
|
+
// Code patterns (priority 10) — English with word boundaries
|
|
107
|
+
[
|
|
108
|
+
/\b(code|coding|program|function|class|bug|debug|refactor|implement)\b/i,
|
|
109
|
+
TaskType.CODE,
|
|
110
|
+
10,
|
|
111
|
+
],
|
|
112
|
+
[
|
|
113
|
+
/\b(javascript|typescript|python|java|rust|go|c\+\+|sql|html|css|react|vue|node|npm|git|api|endpoint|database)\b/i,
|
|
114
|
+
TaskType.CODE,
|
|
115
|
+
10,
|
|
116
|
+
],
|
|
117
|
+
[/```[\s\S]*```/, TaskType.CODE, 10],
|
|
118
|
+
// Code patterns — Chinese (no \b, Chinese chars are not word-boundary compatible)
|
|
119
|
+
[
|
|
120
|
+
/(代码|编程|函数|调试|重构|实现|写[一个]*[代码函数方法])/,
|
|
121
|
+
TaskType.CODE,
|
|
122
|
+
10,
|
|
123
|
+
],
|
|
124
|
+
|
|
125
|
+
// Reasoning patterns (priority 8)
|
|
126
|
+
[
|
|
127
|
+
/\b(analyze|reason|explain why|prove|compare|evaluate)\b/i,
|
|
128
|
+
TaskType.REASONING,
|
|
129
|
+
8,
|
|
130
|
+
],
|
|
131
|
+
[/\b(step.by.step|think.*through)\b/i, TaskType.REASONING, 8],
|
|
132
|
+
[
|
|
133
|
+
/(分析|推理|解释为什么|证明|比较|评估|深度思考|逻辑|逐步|一步一步)/,
|
|
134
|
+
TaskType.REASONING,
|
|
135
|
+
8,
|
|
136
|
+
],
|
|
137
|
+
|
|
138
|
+
// Translation patterns (priority 9)
|
|
139
|
+
[/\b(translate|translation|translate.*to)\b/i, TaskType.TRANSLATE, 9],
|
|
140
|
+
[/(翻译|转换.*语言|英译中|中译英)/, TaskType.TRANSLATE, 9],
|
|
141
|
+
|
|
142
|
+
// Creative patterns (priority 7)
|
|
143
|
+
[
|
|
144
|
+
/\b(write|create|compose|story|poem|essay|blog|article)\b/i,
|
|
145
|
+
TaskType.CREATIVE,
|
|
146
|
+
7,
|
|
147
|
+
],
|
|
148
|
+
[/(写[一篇]*.*[故事诗歌文章博客]|创作|小说|剧本)/, TaskType.CREATIVE, 7],
|
|
149
|
+
|
|
150
|
+
// Fast patterns (priority 5)
|
|
151
|
+
[/\b(quick|brief|short)\b/i, TaskType.FAST, 5],
|
|
152
|
+
[/(简短|快速|简单回答|一句话)/, TaskType.FAST, 5],
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Detect the task type from a user message using keyword matching.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} message - User's input message
|
|
159
|
+
* @returns {{ taskType: string, confidence: number, name: string }}
|
|
160
|
+
*/
|
|
161
|
+
export function detectTaskType(message) {
|
|
162
|
+
if (!message || typeof message !== "string") {
|
|
163
|
+
return {
|
|
164
|
+
taskType: TaskType.CHAT,
|
|
165
|
+
confidence: 0,
|
|
166
|
+
name: TASK_NAMES[TaskType.CHAT],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let bestMatch = null;
|
|
171
|
+
let bestPriority = -1;
|
|
172
|
+
let matchCount = 0;
|
|
173
|
+
|
|
174
|
+
for (const [pattern, taskType, priority] of TASK_PATTERNS) {
|
|
175
|
+
if (pattern.test(message)) {
|
|
176
|
+
matchCount++;
|
|
177
|
+
if (priority > bestPriority) {
|
|
178
|
+
bestPriority = priority;
|
|
179
|
+
bestMatch = taskType;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!bestMatch) {
|
|
185
|
+
return {
|
|
186
|
+
taskType: TaskType.CHAT,
|
|
187
|
+
confidence: 0,
|
|
188
|
+
name: TASK_NAMES[TaskType.CHAT],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Confidence based on match count and priority
|
|
193
|
+
const confidence = Math.min(1, matchCount * 0.3 + bestPriority * 0.07);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
taskType: bestMatch,
|
|
197
|
+
confidence,
|
|
198
|
+
name: TASK_NAMES[bestMatch],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Select the best model for a given provider and task type.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} provider - LLM provider name
|
|
206
|
+
* @param {string} taskType - Task type from TaskType enum
|
|
207
|
+
* @returns {string|null} Model ID, or null if no recommendation
|
|
208
|
+
*/
|
|
209
|
+
export function selectModelForTask(provider, taskType) {
|
|
210
|
+
const taskMap = TASK_MODEL_MAP[taskType];
|
|
211
|
+
if (!taskMap) return null;
|
|
212
|
+
return taskMap[provider] || null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get a human-readable task name.
|
|
217
|
+
*
|
|
218
|
+
* @param {string} taskType - Task type
|
|
219
|
+
* @returns {string}
|
|
220
|
+
*/
|
|
221
|
+
export function getTaskName(taskType) {
|
|
222
|
+
return TASK_NAMES[taskType] || taskType;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get all supported task types.
|
|
227
|
+
*
|
|
228
|
+
* @returns {Object} TaskType enum
|
|
229
|
+
*/
|
|
230
|
+
export function getTaskTypes() {
|
|
231
|
+
return { ...TaskType };
|
|
232
|
+
}
|