@antaif3ng/til-work 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +256 -298
- package/dist/core/config.d.ts +29 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +65 -101
- package/dist/core/config.js.map +1 -1
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +14 -0
- package/dist/core/llm.js.map +1 -1
- package/dist/core/session.d.ts +3 -2
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js +4 -3
- package/dist/core/session.js.map +1 -1
- package/dist/core/skills.d.ts +2 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +9 -0
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +4 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +66 -124
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive.d.ts.map +1 -1
- package/dist/modes/interactive.js +272 -180
- package/dist/modes/interactive.js.map +1 -1
- package/dist/tools/browser.d.ts +10 -0
- package/dist/tools/browser.d.ts.map +1 -0
- package/dist/tools/browser.js +231 -0
- package/dist/tools/browser.js.map +1 -0
- package/dist/tools/computer.d.ts +3 -0
- package/dist/tools/computer.d.ts.map +1 -0
- package/dist/tools/computer.js +251 -0
- package/dist/tools/computer.js.map +1 -0
- package/dist/tools/index.d.ts +5 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +11 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +29 -4
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/screenshot.d.ts +3 -0
- package/dist/tools/screenshot.d.ts.map +1 -0
- package/dist/tools/screenshot.js +113 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/package.json +2 -1
- package/skills/find-skills/SKILL.md +66 -0
- package/skills/playwright-mcp/SKILL.md +90 -0
- package/skills/self-improving-agent/SKILL.md +88 -0
- package/skills/skill-creator/SKILL.md +93 -0
- package/skills/summarize/SKILL.md +55 -0
|
@@ -5,7 +5,7 @@ import * as readline from "node:readline";
|
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { TilSession } from "../core/session.js";
|
|
8
|
-
import {
|
|
8
|
+
import { getConfigPath, loadConfig, saveConfig } from "../core/config.js";
|
|
9
9
|
import { VERSION } from "../version.js";
|
|
10
10
|
import { createToolCallRequestHandler } from "../core/tool-permissions.js";
|
|
11
11
|
import { formatTokenCount } from "../core/pricing.js";
|
|
@@ -15,8 +15,7 @@ import { extractAtReferences } from "../utils/file-processor.js";
|
|
|
15
15
|
const COMMANDS = {
|
|
16
16
|
"/help": "显示帮助信息",
|
|
17
17
|
"/clear": "清空对话历史",
|
|
18
|
-
"/model": "
|
|
19
|
-
"/models": "查看可用模型列表",
|
|
18
|
+
"/model": "查看/切换/管理模型",
|
|
20
19
|
"/skills": "查看已加载的技能",
|
|
21
20
|
"/mcp": "查看已连接的 MCP 服务和工具",
|
|
22
21
|
"/extensions": "查看已加载的扩展和 MCP 服务",
|
|
@@ -52,6 +51,35 @@ function filterEntries(entries, filter) {
|
|
|
52
51
|
return entries;
|
|
53
52
|
return entries.filter((e) => e.label.startsWith(filter));
|
|
54
53
|
}
|
|
54
|
+
function getModelMenuEntries(filter, session) {
|
|
55
|
+
const entries = [];
|
|
56
|
+
const currentId = session.model.id;
|
|
57
|
+
// Current model first
|
|
58
|
+
entries.push({
|
|
59
|
+
label: currentId,
|
|
60
|
+
detail: "当前",
|
|
61
|
+
value: `/model ${currentId}`,
|
|
62
|
+
});
|
|
63
|
+
// Load config synchronously — config.models is already migrated at startup
|
|
64
|
+
const cfg = session.config;
|
|
65
|
+
if (cfg.models) {
|
|
66
|
+
for (const [id, m] of Object.entries(cfg.models)) {
|
|
67
|
+
if (id === currentId)
|
|
68
|
+
continue;
|
|
69
|
+
const parts = [];
|
|
70
|
+
if (m.baseUrl)
|
|
71
|
+
parts.push(m.baseUrl.replace(/https?:\/\//, "").slice(0, 30));
|
|
72
|
+
entries.push({
|
|
73
|
+
label: id,
|
|
74
|
+
detail: parts.join(" ") || "",
|
|
75
|
+
value: `/model ${id}`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!filter)
|
|
80
|
+
return entries;
|
|
81
|
+
return entries.filter((e) => e.label.includes(filter));
|
|
82
|
+
}
|
|
55
83
|
function getFileMenuEntries(prefix, cwd) {
|
|
56
84
|
const suggestions = getFileSuggestions(prefix, cwd);
|
|
57
85
|
return suggestions.slice(0, 10).map((s) => ({
|
|
@@ -180,6 +208,8 @@ export async function runInteractiveMode(options) {
|
|
|
180
208
|
let isStreaming = false;
|
|
181
209
|
let spinner = null;
|
|
182
210
|
let firstTokenReceived = false;
|
|
211
|
+
let insideThinkBlock = false;
|
|
212
|
+
let thinkTagBuffer = "";
|
|
183
213
|
session.subscribe((event) => {
|
|
184
214
|
switch (event.type) {
|
|
185
215
|
case "agent_start":
|
|
@@ -187,12 +217,49 @@ export async function runInteractiveMode(options) {
|
|
|
187
217
|
streamingRawText = "";
|
|
188
218
|
isStreaming = true;
|
|
189
219
|
firstTokenReceived = false;
|
|
220
|
+
insideThinkBlock = false;
|
|
221
|
+
thinkTagBuffer = "";
|
|
190
222
|
spinner = new Spinner("思考中...");
|
|
191
223
|
spinner.start();
|
|
192
224
|
break;
|
|
193
225
|
case "message_update": {
|
|
194
226
|
if (event.delta) {
|
|
195
|
-
|
|
227
|
+
currentOutput += event.delta;
|
|
228
|
+
let text = thinkTagBuffer + event.delta;
|
|
229
|
+
thinkTagBuffer = "";
|
|
230
|
+
let normalText = "";
|
|
231
|
+
let thinkText = "";
|
|
232
|
+
while (text.length > 0) {
|
|
233
|
+
if (insideThinkBlock) {
|
|
234
|
+
const closeIdx = text.indexOf("</think>");
|
|
235
|
+
if (closeIdx === -1) {
|
|
236
|
+
thinkText += text;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
thinkText += text.slice(0, closeIdx);
|
|
240
|
+
text = text.slice(closeIdx + 8);
|
|
241
|
+
insideThinkBlock = false;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const openIdx = text.indexOf("<think>");
|
|
245
|
+
if (openIdx === -1) {
|
|
246
|
+
const partialIdx = text.lastIndexOf("<");
|
|
247
|
+
if (partialIdx !== -1 && partialIdx > text.length - 8 && "<think>".startsWith(text.slice(partialIdx))) {
|
|
248
|
+
thinkTagBuffer = text.slice(partialIdx);
|
|
249
|
+
normalText += text.slice(0, partialIdx);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
normalText += text;
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
normalText += text.slice(0, openIdx);
|
|
257
|
+
text = text.slice(openIdx + 7);
|
|
258
|
+
insideThinkBlock = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const hasOutput = normalText || thinkText;
|
|
262
|
+
if (hasOutput && !firstTokenReceived) {
|
|
196
263
|
firstTokenReceived = true;
|
|
197
264
|
if (spinner) {
|
|
198
265
|
spinner.stop();
|
|
@@ -200,33 +267,47 @@ export async function runInteractiveMode(options) {
|
|
|
200
267
|
}
|
|
201
268
|
process.stdout.write("\n");
|
|
202
269
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
270
|
+
if (thinkText) {
|
|
271
|
+
process.stdout.write(chalk.dim.italic(thinkText));
|
|
272
|
+
streamingRawText += thinkText;
|
|
273
|
+
}
|
|
274
|
+
if (normalText) {
|
|
275
|
+
process.stdout.write(chalk.white(normalText));
|
|
276
|
+
streamingRawText += normalText;
|
|
277
|
+
}
|
|
207
278
|
}
|
|
208
279
|
break;
|
|
209
280
|
}
|
|
210
281
|
case "message_end": {
|
|
211
282
|
if (event.message.role === "assistant") {
|
|
212
283
|
const assistantMsg = event.message;
|
|
213
|
-
|
|
284
|
+
let fullText = assistantMsg.content
|
|
214
285
|
.filter((c) => c.type === "text")
|
|
215
286
|
.map((c) => c.text)
|
|
216
287
|
.join("");
|
|
217
|
-
|
|
288
|
+
const thinkBlocks = extractThinkBlocks(fullText);
|
|
289
|
+
const cleanText = stripThinkBlocks(fullText);
|
|
290
|
+
if ((cleanText.trim() || thinkBlocks.length > 0) && streamingRawText.trim()) {
|
|
218
291
|
const termWidth = process.stdout.columns || 80;
|
|
219
292
|
const visualLines = countVisualLines(streamingRawText, termWidth);
|
|
220
293
|
for (let i = 0; i < visualLines; i++) {
|
|
221
294
|
process.stdout.write("\x1b[2K\x1b[1A");
|
|
222
295
|
}
|
|
223
296
|
process.stdout.write("\x1b[2K\r");
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
297
|
+
if (thinkBlocks.length > 0) {
|
|
298
|
+
const thinkContent = thinkBlocks.join("\n").trim();
|
|
299
|
+
if (thinkContent) {
|
|
300
|
+
process.stdout.write(chalk.dim.italic(thinkContent) + "\n\n");
|
|
301
|
+
}
|
|
227
302
|
}
|
|
228
|
-
|
|
229
|
-
|
|
303
|
+
if (cleanText.trim()) {
|
|
304
|
+
try {
|
|
305
|
+
const rendered = renderMarkdown(cleanText);
|
|
306
|
+
process.stdout.write(rendered + "\n");
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
process.stdout.write(cleanText + "\n");
|
|
310
|
+
}
|
|
230
311
|
}
|
|
231
312
|
}
|
|
232
313
|
streamingRawText = "";
|
|
@@ -284,6 +365,12 @@ export async function runInteractiveMode(options) {
|
|
|
284
365
|
spinner = null;
|
|
285
366
|
}
|
|
286
367
|
isStreaming = false;
|
|
368
|
+
// Check for errors
|
|
369
|
+
const lastMsg = event.messages?.[event.messages.length - 1];
|
|
370
|
+
if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
|
|
371
|
+
const errText = lastMsg.errorMessage || "未知错误";
|
|
372
|
+
process.stdout.write(chalk.red(`\n ✗ 错误: ${errText}\n`));
|
|
373
|
+
}
|
|
287
374
|
// Show per-turn usage summary
|
|
288
375
|
const turnUsage = session.getLastTurnUsageSummary();
|
|
289
376
|
const ctxInfo = session.getContextUsagePercent();
|
|
@@ -400,7 +487,26 @@ export async function runInteractiveMode(options) {
|
|
|
400
487
|
originalTtyWrite.call(this, s, key);
|
|
401
488
|
const newLine = rl.line ?? "";
|
|
402
489
|
const newCursorCol = getCursorCol();
|
|
403
|
-
if (newLine.startsWith("/
|
|
490
|
+
if (newLine.startsWith("/model ")) {
|
|
491
|
+
const modelFilter = newLine.slice("/model ".length);
|
|
492
|
+
if (!["add ", "rm ", "default "].some((sub) => modelFilter.startsWith(sub))) {
|
|
493
|
+
const entries = getModelMenuEntries(modelFilter, session);
|
|
494
|
+
if (entries.length > 0) {
|
|
495
|
+
menu.visible = true;
|
|
496
|
+
menu.items = entries;
|
|
497
|
+
menu.type = "command";
|
|
498
|
+
menu.selectedIndex = 0;
|
|
499
|
+
renderMenu(menu, newCursorCol);
|
|
500
|
+
}
|
|
501
|
+
else if (menu.visible) {
|
|
502
|
+
clearMenu(menu, newCursorCol);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
else if (menu.visible) {
|
|
506
|
+
clearMenu(menu, newCursorCol);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else if (newLine.startsWith("/") && !newLine.includes(" ")) {
|
|
404
510
|
const filtered = filterEntries(allEntries, newLine);
|
|
405
511
|
if (filtered.length > 0) {
|
|
406
512
|
menu.visible = true;
|
|
@@ -505,41 +611,100 @@ async function handleCommand(input, session) {
|
|
|
505
611
|
console.log(chalk.dim("对话已清空。\n"));
|
|
506
612
|
break;
|
|
507
613
|
case "/model": {
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
614
|
+
const sub = args[0];
|
|
615
|
+
// /model — show current + configured models
|
|
616
|
+
if (!sub) {
|
|
617
|
+
const cfg = await loadConfig();
|
|
618
|
+
const configuredModels = cfg.models ?? {};
|
|
619
|
+
const modelIds = Object.keys(configuredModels);
|
|
620
|
+
console.log(chalk.bold(`\n当前模型: ${chalk.cyan(session.model.id)}\n`));
|
|
621
|
+
if (modelIds.length > 0) {
|
|
622
|
+
console.log(chalk.bold("已配置模型:\n"));
|
|
623
|
+
for (const [id, m] of Object.entries(configuredModels)) {
|
|
624
|
+
const marker = id === session.model.id ? chalk.green(" ●") : " ";
|
|
625
|
+
const parts = [];
|
|
626
|
+
if (m.baseUrl)
|
|
627
|
+
parts.push(m.baseUrl);
|
|
628
|
+
if (m.apiKey)
|
|
629
|
+
parts.push(chalk.green("key ✓"));
|
|
630
|
+
console.log(`${marker} ${chalk.white(id)}${parts.length > 0 ? chalk.dim(` ${parts.join(" ")}`) : ""}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
console.log(chalk.dim(" 暂无配置模型。使用 /model add 添加。"));
|
|
635
|
+
}
|
|
636
|
+
console.log(chalk.dim(`\n /model <id> 切换模型`));
|
|
637
|
+
console.log(chalk.dim(` /model add <id> [baseUrl] [apiKey] 添加模型`));
|
|
638
|
+
console.log(chalk.dim(` /model default <id> 设为默认(持久化)`));
|
|
639
|
+
console.log(chalk.dim(` /model rm <id> 删除模型\n`));
|
|
512
640
|
break;
|
|
513
641
|
}
|
|
514
|
-
|
|
642
|
+
// /model default <id> — set persistent default
|
|
643
|
+
if (sub === "default") {
|
|
644
|
+
const modelId = args[1];
|
|
645
|
+
if (!modelId) {
|
|
646
|
+
console.log(chalk.red("\n 用法: /model default <model-id>\n"));
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
const cfg = await loadConfig();
|
|
650
|
+
cfg.model = modelId;
|
|
651
|
+
await saveConfig(cfg);
|
|
515
652
|
session.switchModel(modelId);
|
|
516
|
-
console.log(chalk.green(
|
|
653
|
+
console.log(chalk.green(`\n 默认模型已设为: ${modelId} (已同步切换)\n`));
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
// /model add <id> [baseUrl] [apiKey]
|
|
657
|
+
if (sub === "add") {
|
|
658
|
+
const modelId = args[1];
|
|
659
|
+
if (!modelId) {
|
|
660
|
+
console.log(chalk.red("\n 用法: /model add <model-id> [baseUrl] [apiKey]\n"));
|
|
661
|
+
console.log(chalk.dim(" 示例: /model add deepseek-chat https://api.deepseek.com/v1 sk-xxx\n"));
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
const cfg = await loadConfig();
|
|
665
|
+
if (!cfg.models)
|
|
666
|
+
cfg.models = {};
|
|
667
|
+
const entry = {};
|
|
668
|
+
if (args[2])
|
|
669
|
+
entry.baseUrl = args[2];
|
|
670
|
+
if (args[3])
|
|
671
|
+
entry.apiKey = args[3];
|
|
672
|
+
cfg.models[modelId] = entry;
|
|
673
|
+
await saveConfig(cfg);
|
|
674
|
+
console.log(chalk.green(`\n 已添加: ${modelId}`));
|
|
675
|
+
console.log(chalk.dim(` 切换使用: /model ${modelId}\n`));
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
// /model rm <id>
|
|
679
|
+
if (sub === "rm") {
|
|
680
|
+
const modelId = args[1];
|
|
681
|
+
if (!modelId) {
|
|
682
|
+
console.log(chalk.red("\n 用法: /model rm <model-id>\n"));
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
const cfg = await loadConfig();
|
|
686
|
+
if (!cfg.models?.[modelId]) {
|
|
687
|
+
console.log(chalk.red(`\n 自定义模型 "${modelId}" 不存在\n`));
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
delete cfg.models[modelId];
|
|
691
|
+
await saveConfig(cfg);
|
|
692
|
+
console.log(chalk.green(`\n 已删除: ${modelId}\n`));
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
// /model <id> — switch model
|
|
696
|
+
try {
|
|
697
|
+
session.switchModel(sub);
|
|
698
|
+
console.log(chalk.green(`\n 已切换到: ${sub}\n`));
|
|
517
699
|
}
|
|
518
700
|
catch (err) {
|
|
519
|
-
console.log(chalk.red(
|
|
701
|
+
console.log(chalk.red(`\n 切换失败: ${err.message}\n`));
|
|
520
702
|
}
|
|
521
703
|
break;
|
|
522
704
|
}
|
|
523
|
-
case "/models":
|
|
524
|
-
|
|
525
|
-
const builtinModels = getKnownModels();
|
|
526
|
-
console.log(chalk.bold("\n内置模型:\n"));
|
|
527
|
-
for (const [id, m] of Object.entries(builtinModels)) {
|
|
528
|
-
const marker = id === session.model.id ? chalk.green(" ●") : " ";
|
|
529
|
-
const ctx = m.contextWindow ? chalk.dim(` ${formatTokenCount(m.contextWindow)} ctx`) : "";
|
|
530
|
-
console.log(`${marker} ${id.padEnd(35)} ${chalk.dim(m.provider)}${ctx}`);
|
|
531
|
-
}
|
|
532
|
-
if (cfg.customModels && Object.keys(cfg.customModels).length > 0) {
|
|
533
|
-
console.log(chalk.bold("\n自定义模型:\n"));
|
|
534
|
-
for (const [id, m] of Object.entries(cfg.customModels)) {
|
|
535
|
-
const marker = id === session.model.id ? chalk.green(" ●") : " ";
|
|
536
|
-
const ctx = m.contextWindow ? chalk.dim(` ${formatTokenCount(m.contextWindow)} ctx`) : "";
|
|
537
|
-
console.log(`${marker} ${id.padEnd(35)} ${chalk.dim(m.provider)}${ctx}`);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
console.log(chalk.dim(`\n 当前: ${session.model.id} | 切换: /model <id> | 添加: /config add model <id> <provider>\n`));
|
|
705
|
+
case "/models":
|
|
706
|
+
console.log(chalk.dim(" 已合并到 /model 命令,请直接使用 /model\n"));
|
|
541
707
|
break;
|
|
542
|
-
}
|
|
543
708
|
case "/memory": {
|
|
544
709
|
const mem = session.getMemoryContent();
|
|
545
710
|
if (!mem) {
|
|
@@ -573,151 +738,62 @@ async function handleCommand(input, session) {
|
|
|
573
738
|
case "/config": {
|
|
574
739
|
const config = await loadConfig();
|
|
575
740
|
const sub = args[0];
|
|
576
|
-
if (
|
|
577
|
-
console.log(chalk.bold("\n
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
console.log(`
|
|
582
|
-
console.log(`
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const parts = [];
|
|
589
|
-
if (prov.apiKey)
|
|
590
|
-
parts.push(chalk.green("key ✓"));
|
|
591
|
-
else
|
|
592
|
-
parts.push(chalk.dim("无 key"));
|
|
593
|
-
if (prov.baseUrl)
|
|
594
|
-
parts.push(prov.baseUrl);
|
|
595
|
-
console.log(` ${chalk.cyan(name.padEnd(20))} ${parts.join(" ")}`);
|
|
596
|
-
}
|
|
597
|
-
if (config.customModels && Object.keys(config.customModels).length > 0) {
|
|
598
|
-
console.log(chalk.bold("\n 自定义模型:"));
|
|
599
|
-
for (const [id, m] of Object.entries(config.customModels)) {
|
|
600
|
-
console.log(` ${chalk.cyan(id.padEnd(30))} ${chalk.dim(m.provider)}${m.contextWindow ? ` ctx:${formatTokenCount(m.contextWindow)}` : ""}`);
|
|
601
|
-
}
|
|
741
|
+
if (sub === "check") {
|
|
742
|
+
console.log(chalk.bold("\n配置检测:\n"));
|
|
743
|
+
const m = session.model;
|
|
744
|
+
const provider = m.provider;
|
|
745
|
+
const maskedKey = m.apiKey ? m.apiKey.slice(0, 8) + "..." + m.apiKey.slice(-4) : chalk.red("未设置");
|
|
746
|
+
console.log(` 模型: ${chalk.cyan(m.id)}`);
|
|
747
|
+
console.log(` Provider: ${chalk.cyan(provider)}`);
|
|
748
|
+
console.log(` Base URL: ${chalk.cyan(m.baseUrl || "(默认)")}`);
|
|
749
|
+
console.log(` API Key: ${maskedKey}`);
|
|
750
|
+
if (!m.apiKey) {
|
|
751
|
+
console.log(chalk.red("\n ✗ API Key 未设置,无法连接\n"));
|
|
752
|
+
break;
|
|
602
753
|
}
|
|
603
|
-
console.log(chalk.dim("\n
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
console.log(` ${chalk.cyan("/config")} 查看当前配置`);
|
|
609
|
-
console.log(` ${chalk.cyan("/config set model <id>")} 设置默认模型 (持久化)`);
|
|
610
|
-
console.log(` ${chalk.cyan("/config set key <provider> <key>")} 设置 Provider API Key`);
|
|
611
|
-
console.log(` ${chalk.cyan("/config set url <provider> <url>")} 设置 Provider Base URL`);
|
|
612
|
-
console.log(` ${chalk.cyan("/config add model <id> <provider>")} 添加自定义模型`);
|
|
613
|
-
console.log(` ${chalk.cyan("/config rm model <id>")} 删除自定义模型`);
|
|
614
|
-
console.log();
|
|
615
|
-
console.log(chalk.dim(" 示例:"));
|
|
616
|
-
console.log(chalk.dim(" /config set model gpt-4o"));
|
|
617
|
-
console.log(chalk.dim(" /config set key openai sk-xxx"));
|
|
618
|
-
console.log(chalk.dim(" /config set url openai https://my-proxy.com/v1"));
|
|
619
|
-
console.log(chalk.dim(" /config add model deepseek-chat openai-compatible"));
|
|
620
|
-
console.log();
|
|
621
|
-
break;
|
|
622
|
-
}
|
|
623
|
-
if (sub === "set") {
|
|
624
|
-
const field = args[1];
|
|
625
|
-
if (field === "model") {
|
|
626
|
-
const modelId = args[2];
|
|
627
|
-
if (!modelId) {
|
|
628
|
-
console.log(chalk.red("\n 用法: /config set model <model-id>\n"));
|
|
629
|
-
break;
|
|
630
|
-
}
|
|
631
|
-
const provider = guessProvider(modelId);
|
|
632
|
-
config.defaultModel = { provider, id: modelId };
|
|
633
|
-
await saveConfig(config);
|
|
634
|
-
session.switchModel(modelId);
|
|
635
|
-
console.log(chalk.green(`\n 默认模型已设为: ${modelId} (provider: ${provider})`));
|
|
636
|
-
console.log(chalk.dim(` 已同步切换当前会话模型\n`));
|
|
637
|
-
}
|
|
638
|
-
else if (field === "key") {
|
|
639
|
-
const provider = args[2];
|
|
640
|
-
const key = args[3];
|
|
641
|
-
if (!provider || !key) {
|
|
642
|
-
console.log(chalk.red("\n 用法: /config set key <provider> <api-key>\n"));
|
|
643
|
-
break;
|
|
644
|
-
}
|
|
645
|
-
if (!config.providers[provider])
|
|
646
|
-
config.providers[provider] = {};
|
|
647
|
-
config.providers[provider].apiKey = key;
|
|
648
|
-
await saveConfig(config);
|
|
649
|
-
console.log(chalk.green(`\n 已设置 ${provider} 的 API Key\n`));
|
|
650
|
-
}
|
|
651
|
-
else if (field === "url") {
|
|
652
|
-
const provider = args[2];
|
|
653
|
-
const url = args[3];
|
|
654
|
-
if (!provider || !url) {
|
|
655
|
-
console.log(chalk.red("\n 用法: /config set url <provider> <base-url>\n"));
|
|
656
|
-
break;
|
|
657
|
-
}
|
|
658
|
-
if (!config.providers[provider])
|
|
659
|
-
config.providers[provider] = {};
|
|
660
|
-
config.providers[provider].baseUrl = url;
|
|
661
|
-
await saveConfig(config);
|
|
662
|
-
console.log(chalk.green(`\n 已设置 ${provider} 的 Base URL: ${url}\n`));
|
|
754
|
+
console.log(chalk.dim("\n 正在测试 API 连接..."));
|
|
755
|
+
try {
|
|
756
|
+
const { completeLLM } = await import("../core/llm.js");
|
|
757
|
+
const text = await completeLLM(m, "You are a test.", "Say OK", 32);
|
|
758
|
+
console.log(chalk.green(` ✓ 连接成功! 模型响应: "${text.slice(0, 80)}"`));
|
|
663
759
|
}
|
|
664
|
-
|
|
665
|
-
console.log(chalk.red(
|
|
666
|
-
console.log(chalk.dim(" 查看帮助: /config help\n"));
|
|
760
|
+
catch (err) {
|
|
761
|
+
console.log(chalk.red(` ✗ 连接失败: ${err.message}`));
|
|
667
762
|
}
|
|
763
|
+
console.log();
|
|
668
764
|
break;
|
|
669
765
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
name: modelId,
|
|
685
|
-
maxTokens: 8192,
|
|
686
|
-
};
|
|
687
|
-
await saveConfig(config);
|
|
688
|
-
console.log(chalk.green(`\n 已添加自定义模型: ${modelId} (provider: ${provider})`));
|
|
689
|
-
console.log(chalk.dim(` 切换使用: /model ${modelId}\n`));
|
|
690
|
-
}
|
|
691
|
-
else {
|
|
692
|
-
console.log(chalk.red(`\n 未知类型: ${what}。支持: model`));
|
|
693
|
-
console.log(chalk.dim(" 查看帮助: /config help\n"));
|
|
766
|
+
console.log(chalk.bold("\n当前配置:\n"));
|
|
767
|
+
console.log(` 模型: ${chalk.cyan(session.model.id)}`);
|
|
768
|
+
console.log(` Base URL: ${chalk.cyan(session.model.baseUrl || "(默认)")}`);
|
|
769
|
+
console.log(` API Key: ${session.model.apiKey ? chalk.green("已配置") : chalk.red("未设置")}`);
|
|
770
|
+
console.log(` Session: ${chalk.cyan(session.sessionManager.shortId)}`);
|
|
771
|
+
if (config.models && Object.keys(config.models).length > 0) {
|
|
772
|
+
console.log(chalk.bold("\n 自定义模型:"));
|
|
773
|
+
for (const [id, m] of Object.entries(config.models)) {
|
|
774
|
+
const parts = [];
|
|
775
|
+
if (m.apiKey)
|
|
776
|
+
parts.push(chalk.green("key ✓"));
|
|
777
|
+
if (m.baseUrl)
|
|
778
|
+
parts.push(m.baseUrl);
|
|
779
|
+
console.log(` ${chalk.cyan(id.padEnd(25))} ${parts.join(" ") || chalk.dim("(继承全局)")}`);
|
|
694
780
|
}
|
|
695
|
-
break;
|
|
696
781
|
}
|
|
697
|
-
if (
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
if (
|
|
706
|
-
console.log(chalk.
|
|
707
|
-
break;
|
|
782
|
+
if (Object.keys(config.providers).length > 0) {
|
|
783
|
+
console.log(chalk.bold("\n Provider 凭证:"));
|
|
784
|
+
for (const [name, prov] of Object.entries(config.providers)) {
|
|
785
|
+
const parts = [];
|
|
786
|
+
if (prov.apiKey)
|
|
787
|
+
parts.push(chalk.green("key ✓"));
|
|
788
|
+
if (prov.baseUrl)
|
|
789
|
+
parts.push(prov.baseUrl);
|
|
790
|
+
if (parts.length > 0) {
|
|
791
|
+
console.log(` ${chalk.cyan(name.padEnd(20))} ${parts.join(" ")}`);
|
|
708
792
|
}
|
|
709
|
-
delete config.customModels[modelId];
|
|
710
|
-
await saveConfig(config);
|
|
711
|
-
console.log(chalk.green(`\n 已删除自定义模型: ${modelId}\n`));
|
|
712
793
|
}
|
|
713
|
-
else {
|
|
714
|
-
console.log(chalk.red(`\n 未知类型: ${what}。支持: model`));
|
|
715
|
-
console.log(chalk.dim(" 查看帮助: /config help\n"));
|
|
716
|
-
}
|
|
717
|
-
break;
|
|
718
794
|
}
|
|
719
|
-
console.log(chalk.
|
|
720
|
-
console.log(chalk.dim(
|
|
795
|
+
console.log(chalk.dim(`\n 配置文件: ${getConfigPath()}`));
|
|
796
|
+
console.log(chalk.dim(` /config check 测试连接 | /model 管理模型 | til --setup 重新配置\n`));
|
|
721
797
|
break;
|
|
722
798
|
}
|
|
723
799
|
case "/skills": {
|
|
@@ -726,12 +802,14 @@ async function handleCommand(input, session) {
|
|
|
726
802
|
console.log(chalk.dim("\n 暂无技能。将 SKILL.md 文件放入 ~/.til/skills/ 或 .til/skills/ 即可加载。\n"));
|
|
727
803
|
}
|
|
728
804
|
else {
|
|
805
|
+
const sourceLabel = { builtin: "内置", user: "用户", project: "项目", path: "路径" };
|
|
729
806
|
console.log(chalk.bold(`\n已加载技能 (${skills.length}):\n`));
|
|
730
807
|
for (const s of skills) {
|
|
731
|
-
|
|
732
|
-
console.log(` ${chalk.dim(s.
|
|
808
|
+
const tag = chalk.dim(`[${sourceLabel[s.source] || s.source}]`);
|
|
809
|
+
console.log(` ${chalk.cyan(s.name.padEnd(24))} ${tag} ${chalk.dim(s.description.slice(0, 50))}`);
|
|
733
810
|
}
|
|
734
|
-
console.log(chalk.dim("\n 使用 /skill:<名称>
|
|
811
|
+
console.log(chalk.dim("\n 使用 /skill:<名称> 查看技能详情。"));
|
|
812
|
+
console.log(chalk.dim(" 自定义: ~/.til/skills/ 或 .til/skills/\n"));
|
|
735
813
|
}
|
|
736
814
|
break;
|
|
737
815
|
}
|
|
@@ -909,6 +987,20 @@ function getVisualWidth(str) {
|
|
|
909
987
|
}
|
|
910
988
|
return w;
|
|
911
989
|
}
|
|
990
|
+
function extractThinkBlocks(text) {
|
|
991
|
+
const blocks = [];
|
|
992
|
+
const re = /<think>([\s\S]*?)<\/think>/g;
|
|
993
|
+
let m;
|
|
994
|
+
while ((m = re.exec(text)) !== null) {
|
|
995
|
+
const content = m[1].trim();
|
|
996
|
+
if (content)
|
|
997
|
+
blocks.push(content);
|
|
998
|
+
}
|
|
999
|
+
return blocks;
|
|
1000
|
+
}
|
|
1001
|
+
function stripThinkBlocks(text) {
|
|
1002
|
+
return text.replace(/<think>[\s\S]*?<\/think>\s*/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
1003
|
+
}
|
|
912
1004
|
function countVisualLines(text, termWidth) {
|
|
913
1005
|
if (termWidth <= 0)
|
|
914
1006
|
return text.split("\n").length;
|