@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.
Files changed (50) hide show
  1. package/README.md +256 -298
  2. package/dist/core/config.d.ts +29 -11
  3. package/dist/core/config.d.ts.map +1 -1
  4. package/dist/core/config.js +65 -101
  5. package/dist/core/config.js.map +1 -1
  6. package/dist/core/llm.d.ts.map +1 -1
  7. package/dist/core/llm.js +14 -0
  8. package/dist/core/llm.js.map +1 -1
  9. package/dist/core/session.d.ts +3 -2
  10. package/dist/core/session.d.ts.map +1 -1
  11. package/dist/core/session.js +4 -3
  12. package/dist/core/session.js.map +1 -1
  13. package/dist/core/skills.d.ts +2 -1
  14. package/dist/core/skills.d.ts.map +1 -1
  15. package/dist/core/skills.js +9 -0
  16. package/dist/core/skills.js.map +1 -1
  17. package/dist/core/system-prompt.d.ts.map +1 -1
  18. package/dist/core/system-prompt.js +4 -1
  19. package/dist/core/system-prompt.js.map +1 -1
  20. package/dist/main.d.ts.map +1 -1
  21. package/dist/main.js +66 -124
  22. package/dist/main.js.map +1 -1
  23. package/dist/modes/interactive.d.ts.map +1 -1
  24. package/dist/modes/interactive.js +272 -180
  25. package/dist/modes/interactive.js.map +1 -1
  26. package/dist/tools/browser.d.ts +10 -0
  27. package/dist/tools/browser.d.ts.map +1 -0
  28. package/dist/tools/browser.js +231 -0
  29. package/dist/tools/browser.js.map +1 -0
  30. package/dist/tools/computer.d.ts +3 -0
  31. package/dist/tools/computer.d.ts.map +1 -0
  32. package/dist/tools/computer.js +251 -0
  33. package/dist/tools/computer.js.map +1 -0
  34. package/dist/tools/index.d.ts +5 -2
  35. package/dist/tools/index.d.ts.map +1 -1
  36. package/dist/tools/index.js +11 -2
  37. package/dist/tools/index.js.map +1 -1
  38. package/dist/tools/read.d.ts.map +1 -1
  39. package/dist/tools/read.js +29 -4
  40. package/dist/tools/read.js.map +1 -1
  41. package/dist/tools/screenshot.d.ts +3 -0
  42. package/dist/tools/screenshot.d.ts.map +1 -0
  43. package/dist/tools/screenshot.js +113 -0
  44. package/dist/tools/screenshot.js.map +1 -0
  45. package/package.json +2 -1
  46. package/skills/find-skills/SKILL.md +66 -0
  47. package/skills/playwright-mcp/SKILL.md +90 -0
  48. package/skills/self-improving-agent/SKILL.md +88 -0
  49. package/skills/skill-creator/SKILL.md +93 -0
  50. 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 { getKnownModels, loadConfig, saveConfig, guessProvider } from "../core/config.js";
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": "切换模型(如 /model gpt-4o)",
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
- if (!firstTokenReceived) {
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
- // Stream raw text for responsiveness
204
- process.stdout.write(chalk.white(event.delta));
205
- streamingRawText += event.delta;
206
- currentOutput += event.delta;
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
- const fullText = assistantMsg.content
284
+ let fullText = assistantMsg.content
214
285
  .filter((c) => c.type === "text")
215
286
  .map((c) => c.text)
216
287
  .join("");
217
- if (fullText.trim() && streamingRawText.trim()) {
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
- try {
225
- const rendered = renderMarkdown(fullText);
226
- process.stdout.write(rendered + "\n");
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
- catch {
229
- process.stdout.write(fullText + "\n");
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("/") && !newLine.includes(" ")) {
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 modelId = args[0];
509
- if (!modelId) {
510
- console.log(chalk.yellow(`当前模型: ${session.model.id}`));
511
- console.log(chalk.dim("用法: /model <model-id>\n"));
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
- try {
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(`已切换到: ${modelId}\n`));
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(`切换失败: ${err.message}\n`));
701
+ console.log(chalk.red(`\n 切换失败: ${err.message}\n`));
520
702
  }
521
703
  break;
522
704
  }
523
- case "/models": {
524
- const cfg = await loadConfig();
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 (!sub) {
577
- console.log(chalk.bold("\n当前配置:\n"));
578
- console.log(` 模型: ${chalk.cyan(session.model.id)}`);
579
- console.log(` Provider: ${chalk.cyan(session.model.provider)}`);
580
- console.log(` Base URL: ${chalk.cyan(session.model.baseUrl || "(默认)")}`);
581
- console.log(` API Key: ${session.model.apiKey ? chalk.green("已配置") : chalk.red("未设置")}`);
582
- console.log(` Session: ${chalk.cyan(session.sessionManager.shortId)}`);
583
- if (session.model.headers && Object.keys(session.model.headers).length > 0) {
584
- console.log(` Headers: ${chalk.dim(Object.keys(session.model.headers).join(", "))}`);
585
- }
586
- console.log(chalk.bold("\n 所有 Provider:"));
587
- for (const [name, prov] of Object.entries(config.providers)) {
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 用法: /config set model|key|url|add-model (查看 /config help)\n"));
604
- break;
605
- }
606
- if (sub === "help") {
607
- console.log(chalk.bold("\n/config 子命令:\n"));
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
- else {
665
- console.log(chalk.red(`\n 未知字段: ${field}。支持: model, key, url`));
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
- if (sub === "add") {
671
- const what = args[1];
672
- if (what === "model") {
673
- const modelId = args[2];
674
- const provider = args[3];
675
- if (!modelId || !provider) {
676
- console.log(chalk.red("\n 用法: /config add model <model-id> <provider>\n"));
677
- console.log(chalk.dim(" provider 可选: anthropic, openai, openai-compatible, google\n"));
678
- break;
679
- }
680
- if (!config.customModels)
681
- config.customModels = {};
682
- config.customModels[modelId] = {
683
- provider,
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 (sub === "rm") {
698
- const what = args[1];
699
- if (what === "model") {
700
- const modelId = args[2];
701
- if (!modelId) {
702
- console.log(chalk.red("\n 用法: /config rm model <model-id>\n"));
703
- break;
704
- }
705
- if (!config.customModels?.[modelId]) {
706
- console.log(chalk.red(`\n 自定义模型 "${modelId}" 不存在 (内置模型不可删除)\n`));
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.red(`\n 未知子命令: ${sub}`));
720
- console.log(chalk.dim(" 查看帮助: /config help\n"));
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
- console.log(` ${chalk.cyan(s.name.padEnd(24))} ${chalk.dim(s.description.slice(0, 60))}`);
732
- console.log(` ${chalk.dim(s.filePath)}`);
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:<名称> 查看技能详情。\n"));
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;