@antaif3ng/til-work 0.1.1 → 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 +68 -125
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive.d.ts.map +1 -1
- package/dist/modes/interactive.js +277 -182
- 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/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.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,8 @@ 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
|
+
import { VERSION } from "../version.js";
|
|
9
10
|
import { createToolCallRequestHandler } from "../core/tool-permissions.js";
|
|
10
11
|
import { formatTokenCount } from "../core/pricing.js";
|
|
11
12
|
import { renderMarkdown } from "../core/markdown.js";
|
|
@@ -14,8 +15,7 @@ import { extractAtReferences } from "../utils/file-processor.js";
|
|
|
14
15
|
const COMMANDS = {
|
|
15
16
|
"/help": "显示帮助信息",
|
|
16
17
|
"/clear": "清空对话历史",
|
|
17
|
-
"/model": "
|
|
18
|
-
"/models": "查看可用模型列表",
|
|
18
|
+
"/model": "查看/切换/管理模型",
|
|
19
19
|
"/skills": "查看已加载的技能",
|
|
20
20
|
"/mcp": "查看已连接的 MCP 服务和工具",
|
|
21
21
|
"/extensions": "查看已加载的扩展和 MCP 服务",
|
|
@@ -51,6 +51,35 @@ function filterEntries(entries, filter) {
|
|
|
51
51
|
return entries;
|
|
52
52
|
return entries.filter((e) => e.label.startsWith(filter));
|
|
53
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
|
+
}
|
|
54
83
|
function getFileMenuEntries(prefix, cwd) {
|
|
55
84
|
const suggestions = getFileSuggestions(prefix, cwd);
|
|
56
85
|
return suggestions.slice(0, 10).map((s) => ({
|
|
@@ -169,13 +198,18 @@ export async function runInteractiveMode(options) {
|
|
|
169
198
|
if (!options.onToolCallRequest) {
|
|
170
199
|
options.onToolCallRequest = createToolCallRequestHandler(true, permissionPrompt);
|
|
171
200
|
}
|
|
201
|
+
const startupSpinner = new Spinner("启动中...");
|
|
202
|
+
startupSpinner.start();
|
|
172
203
|
const session = await TilSession.create(options);
|
|
204
|
+
startupSpinner.stop();
|
|
173
205
|
printBanner(session);
|
|
174
206
|
let currentOutput = "";
|
|
175
207
|
let streamingRawText = "";
|
|
176
208
|
let isStreaming = false;
|
|
177
209
|
let spinner = null;
|
|
178
210
|
let firstTokenReceived = false;
|
|
211
|
+
let insideThinkBlock = false;
|
|
212
|
+
let thinkTagBuffer = "";
|
|
179
213
|
session.subscribe((event) => {
|
|
180
214
|
switch (event.type) {
|
|
181
215
|
case "agent_start":
|
|
@@ -183,12 +217,49 @@ export async function runInteractiveMode(options) {
|
|
|
183
217
|
streamingRawText = "";
|
|
184
218
|
isStreaming = true;
|
|
185
219
|
firstTokenReceived = false;
|
|
220
|
+
insideThinkBlock = false;
|
|
221
|
+
thinkTagBuffer = "";
|
|
186
222
|
spinner = new Spinner("思考中...");
|
|
187
223
|
spinner.start();
|
|
188
224
|
break;
|
|
189
225
|
case "message_update": {
|
|
190
226
|
if (event.delta) {
|
|
191
|
-
|
|
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) {
|
|
192
263
|
firstTokenReceived = true;
|
|
193
264
|
if (spinner) {
|
|
194
265
|
spinner.stop();
|
|
@@ -196,33 +267,47 @@ export async function runInteractiveMode(options) {
|
|
|
196
267
|
}
|
|
197
268
|
process.stdout.write("\n");
|
|
198
269
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
}
|
|
203
278
|
}
|
|
204
279
|
break;
|
|
205
280
|
}
|
|
206
281
|
case "message_end": {
|
|
207
282
|
if (event.message.role === "assistant") {
|
|
208
283
|
const assistantMsg = event.message;
|
|
209
|
-
|
|
284
|
+
let fullText = assistantMsg.content
|
|
210
285
|
.filter((c) => c.type === "text")
|
|
211
286
|
.map((c) => c.text)
|
|
212
287
|
.join("");
|
|
213
|
-
|
|
288
|
+
const thinkBlocks = extractThinkBlocks(fullText);
|
|
289
|
+
const cleanText = stripThinkBlocks(fullText);
|
|
290
|
+
if ((cleanText.trim() || thinkBlocks.length > 0) && streamingRawText.trim()) {
|
|
214
291
|
const termWidth = process.stdout.columns || 80;
|
|
215
292
|
const visualLines = countVisualLines(streamingRawText, termWidth);
|
|
216
293
|
for (let i = 0; i < visualLines; i++) {
|
|
217
294
|
process.stdout.write("\x1b[2K\x1b[1A");
|
|
218
295
|
}
|
|
219
296
|
process.stdout.write("\x1b[2K\r");
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
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
|
+
}
|
|
223
302
|
}
|
|
224
|
-
|
|
225
|
-
|
|
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
|
+
}
|
|
226
311
|
}
|
|
227
312
|
}
|
|
228
313
|
streamingRawText = "";
|
|
@@ -280,6 +365,12 @@ export async function runInteractiveMode(options) {
|
|
|
280
365
|
spinner = null;
|
|
281
366
|
}
|
|
282
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
|
+
}
|
|
283
374
|
// Show per-turn usage summary
|
|
284
375
|
const turnUsage = session.getLastTurnUsageSummary();
|
|
285
376
|
const ctxInfo = session.getContextUsagePercent();
|
|
@@ -396,7 +487,26 @@ export async function runInteractiveMode(options) {
|
|
|
396
487
|
originalTtyWrite.call(this, s, key);
|
|
397
488
|
const newLine = rl.line ?? "";
|
|
398
489
|
const newCursorCol = getCursorCol();
|
|
399
|
-
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(" ")) {
|
|
400
510
|
const filtered = filterEntries(allEntries, newLine);
|
|
401
511
|
if (filtered.length > 0) {
|
|
402
512
|
menu.visible = true;
|
|
@@ -501,41 +611,100 @@ async function handleCommand(input, session) {
|
|
|
501
611
|
console.log(chalk.dim("对话已清空。\n"));
|
|
502
612
|
break;
|
|
503
613
|
case "/model": {
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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`));
|
|
508
640
|
break;
|
|
509
641
|
}
|
|
510
|
-
|
|
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);
|
|
511
652
|
session.switchModel(modelId);
|
|
512
|
-
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`));
|
|
513
699
|
}
|
|
514
700
|
catch (err) {
|
|
515
|
-
console.log(chalk.red(
|
|
701
|
+
console.log(chalk.red(`\n 切换失败: ${err.message}\n`));
|
|
516
702
|
}
|
|
517
703
|
break;
|
|
518
704
|
}
|
|
519
|
-
case "/models":
|
|
520
|
-
|
|
521
|
-
const builtinModels = getKnownModels();
|
|
522
|
-
console.log(chalk.bold("\n内置模型:\n"));
|
|
523
|
-
for (const [id, m] of Object.entries(builtinModels)) {
|
|
524
|
-
const marker = id === session.model.id ? chalk.green(" ●") : " ";
|
|
525
|
-
const ctx = m.contextWindow ? chalk.dim(` ${formatTokenCount(m.contextWindow)} ctx`) : "";
|
|
526
|
-
console.log(`${marker} ${id.padEnd(35)} ${chalk.dim(m.provider)}${ctx}`);
|
|
527
|
-
}
|
|
528
|
-
if (cfg.customModels && Object.keys(cfg.customModels).length > 0) {
|
|
529
|
-
console.log(chalk.bold("\n自定义模型:\n"));
|
|
530
|
-
for (const [id, m] of Object.entries(cfg.customModels)) {
|
|
531
|
-
const marker = id === session.model.id ? chalk.green(" ●") : " ";
|
|
532
|
-
const ctx = m.contextWindow ? chalk.dim(` ${formatTokenCount(m.contextWindow)} ctx`) : "";
|
|
533
|
-
console.log(`${marker} ${id.padEnd(35)} ${chalk.dim(m.provider)}${ctx}`);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
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"));
|
|
537
707
|
break;
|
|
538
|
-
}
|
|
539
708
|
case "/memory": {
|
|
540
709
|
const mem = session.getMemoryContent();
|
|
541
710
|
if (!mem) {
|
|
@@ -569,151 +738,62 @@ async function handleCommand(input, session) {
|
|
|
569
738
|
case "/config": {
|
|
570
739
|
const config = await loadConfig();
|
|
571
740
|
const sub = args[0];
|
|
572
|
-
if (
|
|
573
|
-
console.log(chalk.bold("\n
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
console.log(`
|
|
578
|
-
console.log(`
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const parts = [];
|
|
585
|
-
if (prov.apiKey)
|
|
586
|
-
parts.push(chalk.green("key ✓"));
|
|
587
|
-
else
|
|
588
|
-
parts.push(chalk.dim("无 key"));
|
|
589
|
-
if (prov.baseUrl)
|
|
590
|
-
parts.push(prov.baseUrl);
|
|
591
|
-
console.log(` ${chalk.cyan(name.padEnd(20))} ${parts.join(" ")}`);
|
|
592
|
-
}
|
|
593
|
-
if (config.customModels && Object.keys(config.customModels).length > 0) {
|
|
594
|
-
console.log(chalk.bold("\n 自定义模型:"));
|
|
595
|
-
for (const [id, m] of Object.entries(config.customModels)) {
|
|
596
|
-
console.log(` ${chalk.cyan(id.padEnd(30))} ${chalk.dim(m.provider)}${m.contextWindow ? ` ctx:${formatTokenCount(m.contextWindow)}` : ""}`);
|
|
597
|
-
}
|
|
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;
|
|
598
753
|
}
|
|
599
|
-
console.log(chalk.dim("\n
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
console.log(` ${chalk.cyan("/config")} 查看当前配置`);
|
|
605
|
-
console.log(` ${chalk.cyan("/config set model <id>")} 设置默认模型 (持久化)`);
|
|
606
|
-
console.log(` ${chalk.cyan("/config set key <provider> <key>")} 设置 Provider API Key`);
|
|
607
|
-
console.log(` ${chalk.cyan("/config set url <provider> <url>")} 设置 Provider Base URL`);
|
|
608
|
-
console.log(` ${chalk.cyan("/config add model <id> <provider>")} 添加自定义模型`);
|
|
609
|
-
console.log(` ${chalk.cyan("/config rm model <id>")} 删除自定义模型`);
|
|
610
|
-
console.log();
|
|
611
|
-
console.log(chalk.dim(" 示例:"));
|
|
612
|
-
console.log(chalk.dim(" /config set model gpt-4o"));
|
|
613
|
-
console.log(chalk.dim(" /config set key openai sk-xxx"));
|
|
614
|
-
console.log(chalk.dim(" /config set url openai https://my-proxy.com/v1"));
|
|
615
|
-
console.log(chalk.dim(" /config add model deepseek-chat openai-compatible"));
|
|
616
|
-
console.log();
|
|
617
|
-
break;
|
|
618
|
-
}
|
|
619
|
-
if (sub === "set") {
|
|
620
|
-
const field = args[1];
|
|
621
|
-
if (field === "model") {
|
|
622
|
-
const modelId = args[2];
|
|
623
|
-
if (!modelId) {
|
|
624
|
-
console.log(chalk.red("\n 用法: /config set model <model-id>\n"));
|
|
625
|
-
break;
|
|
626
|
-
}
|
|
627
|
-
const provider = guessProvider(modelId);
|
|
628
|
-
config.defaultModel = { provider, id: modelId };
|
|
629
|
-
await saveConfig(config);
|
|
630
|
-
session.switchModel(modelId);
|
|
631
|
-
console.log(chalk.green(`\n 默认模型已设为: ${modelId} (provider: ${provider})`));
|
|
632
|
-
console.log(chalk.dim(` 已同步切换当前会话模型\n`));
|
|
633
|
-
}
|
|
634
|
-
else if (field === "key") {
|
|
635
|
-
const provider = args[2];
|
|
636
|
-
const key = args[3];
|
|
637
|
-
if (!provider || !key) {
|
|
638
|
-
console.log(chalk.red("\n 用法: /config set key <provider> <api-key>\n"));
|
|
639
|
-
break;
|
|
640
|
-
}
|
|
641
|
-
if (!config.providers[provider])
|
|
642
|
-
config.providers[provider] = {};
|
|
643
|
-
config.providers[provider].apiKey = key;
|
|
644
|
-
await saveConfig(config);
|
|
645
|
-
console.log(chalk.green(`\n 已设置 ${provider} 的 API Key\n`));
|
|
646
|
-
}
|
|
647
|
-
else if (field === "url") {
|
|
648
|
-
const provider = args[2];
|
|
649
|
-
const url = args[3];
|
|
650
|
-
if (!provider || !url) {
|
|
651
|
-
console.log(chalk.red("\n 用法: /config set url <provider> <base-url>\n"));
|
|
652
|
-
break;
|
|
653
|
-
}
|
|
654
|
-
if (!config.providers[provider])
|
|
655
|
-
config.providers[provider] = {};
|
|
656
|
-
config.providers[provider].baseUrl = url;
|
|
657
|
-
await saveConfig(config);
|
|
658
|
-
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)}"`));
|
|
659
759
|
}
|
|
660
|
-
|
|
661
|
-
console.log(chalk.red(
|
|
662
|
-
console.log(chalk.dim(" 查看帮助: /config help\n"));
|
|
760
|
+
catch (err) {
|
|
761
|
+
console.log(chalk.red(` ✗ 连接失败: ${err.message}`));
|
|
663
762
|
}
|
|
763
|
+
console.log();
|
|
664
764
|
break;
|
|
665
765
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
name: modelId,
|
|
681
|
-
maxTokens: 8192,
|
|
682
|
-
};
|
|
683
|
-
await saveConfig(config);
|
|
684
|
-
console.log(chalk.green(`\n 已添加自定义模型: ${modelId} (provider: ${provider})`));
|
|
685
|
-
console.log(chalk.dim(` 切换使用: /model ${modelId}\n`));
|
|
686
|
-
}
|
|
687
|
-
else {
|
|
688
|
-
console.log(chalk.red(`\n 未知类型: ${what}。支持: model`));
|
|
689
|
-
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("(继承全局)")}`);
|
|
690
780
|
}
|
|
691
|
-
break;
|
|
692
781
|
}
|
|
693
|
-
if (
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const
|
|
697
|
-
if (
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
if (
|
|
702
|
-
console.log(chalk.
|
|
703
|
-
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(" ")}`);
|
|
704
792
|
}
|
|
705
|
-
delete config.customModels[modelId];
|
|
706
|
-
await saveConfig(config);
|
|
707
|
-
console.log(chalk.green(`\n 已删除自定义模型: ${modelId}\n`));
|
|
708
793
|
}
|
|
709
|
-
else {
|
|
710
|
-
console.log(chalk.red(`\n 未知类型: ${what}。支持: model`));
|
|
711
|
-
console.log(chalk.dim(" 查看帮助: /config help\n"));
|
|
712
|
-
}
|
|
713
|
-
break;
|
|
714
794
|
}
|
|
715
|
-
console.log(chalk.
|
|
716
|
-
console.log(chalk.dim(
|
|
795
|
+
console.log(chalk.dim(`\n 配置文件: ${getConfigPath()}`));
|
|
796
|
+
console.log(chalk.dim(` /config check 测试连接 | /model 管理模型 | til --setup 重新配置\n`));
|
|
717
797
|
break;
|
|
718
798
|
}
|
|
719
799
|
case "/skills": {
|
|
@@ -722,12 +802,14 @@ async function handleCommand(input, session) {
|
|
|
722
802
|
console.log(chalk.dim("\n 暂无技能。将 SKILL.md 文件放入 ~/.til/skills/ 或 .til/skills/ 即可加载。\n"));
|
|
723
803
|
}
|
|
724
804
|
else {
|
|
805
|
+
const sourceLabel = { builtin: "内置", user: "用户", project: "项目", path: "路径" };
|
|
725
806
|
console.log(chalk.bold(`\n已加载技能 (${skills.length}):\n`));
|
|
726
807
|
for (const s of skills) {
|
|
727
|
-
|
|
728
|
-
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))}`);
|
|
729
810
|
}
|
|
730
|
-
console.log(chalk.dim("\n 使用 /skill:<名称>
|
|
811
|
+
console.log(chalk.dim("\n 使用 /skill:<名称> 查看技能详情。"));
|
|
812
|
+
console.log(chalk.dim(" 自定义: ~/.til/skills/ 或 .til/skills/\n"));
|
|
731
813
|
}
|
|
732
814
|
break;
|
|
733
815
|
}
|
|
@@ -905,6 +987,20 @@ function getVisualWidth(str) {
|
|
|
905
987
|
}
|
|
906
988
|
return w;
|
|
907
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
|
+
}
|
|
908
1004
|
function countVisualLines(text, termWidth) {
|
|
909
1005
|
if (termWidth <= 0)
|
|
910
1006
|
return text.split("\n").length;
|
|
@@ -917,12 +1013,11 @@ function countVisualLines(text, termWidth) {
|
|
|
917
1013
|
return total;
|
|
918
1014
|
}
|
|
919
1015
|
function printBanner(session) {
|
|
920
|
-
const VERSION = "v0.1.1";
|
|
921
1016
|
const modelId = session.model.name || session.model.id;
|
|
922
1017
|
const dir = session.cwd.replace(process.env.HOME || "", "~");
|
|
923
1018
|
const sm = session.sessionManager;
|
|
924
1019
|
console.log();
|
|
925
|
-
console.log(chalk.bold.cyan(">_") + " " + chalk.bold("欢迎使用千岛湖
|
|
1020
|
+
console.log(chalk.bold.cyan(">_") + " " + chalk.bold("欢迎使用千岛湖 Til-Work") + " " + chalk.dim(`v${VERSION}`));
|
|
926
1021
|
console.log();
|
|
927
1022
|
const sessionLabel = sm.isResumed
|
|
928
1023
|
? `${chalk.white(sm.shortId)} ${chalk.dim(`(已恢复, ${sm.restoredMessageCount} 条消息)`)}`
|