@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.
Files changed (54) 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 +68 -125
  22. package/dist/main.js.map +1 -1
  23. package/dist/modes/interactive.d.ts.map +1 -1
  24. package/dist/modes/interactive.js +277 -182
  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/dist/version.d.ts +3 -0
  46. package/dist/version.d.ts.map +1 -0
  47. package/dist/version.js +6 -0
  48. package/dist/version.js.map +1 -0
  49. package/package.json +2 -1
  50. package/skills/find-skills/SKILL.md +66 -0
  51. package/skills/playwright-mcp/SKILL.md +90 -0
  52. package/skills/self-improving-agent/SKILL.md +88 -0
  53. package/skills/skill-creator/SKILL.md +93 -0
  54. 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 { getKnownModels, loadConfig, saveConfig, guessProvider } from "../core/config.js";
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": "切换模型(如 /model gpt-4o)",
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
- 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) {
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
- // Stream raw text for responsiveness
200
- process.stdout.write(chalk.white(event.delta));
201
- streamingRawText += event.delta;
202
- 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
+ }
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
- const fullText = assistantMsg.content
284
+ let fullText = assistantMsg.content
210
285
  .filter((c) => c.type === "text")
211
286
  .map((c) => c.text)
212
287
  .join("");
213
- if (fullText.trim() && streamingRawText.trim()) {
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
- try {
221
- const rendered = renderMarkdown(fullText);
222
- 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
+ }
223
302
  }
224
- catch {
225
- 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
+ }
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("/") && !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(" ")) {
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 modelId = args[0];
505
- if (!modelId) {
506
- console.log(chalk.yellow(`当前模型: ${session.model.id}`));
507
- 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`));
508
640
  break;
509
641
  }
510
- 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);
511
652
  session.switchModel(modelId);
512
- 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`));
513
699
  }
514
700
  catch (err) {
515
- console.log(chalk.red(`切换失败: ${err.message}\n`));
701
+ console.log(chalk.red(`\n 切换失败: ${err.message}\n`));
516
702
  }
517
703
  break;
518
704
  }
519
- case "/models": {
520
- const cfg = await loadConfig();
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 (!sub) {
573
- console.log(chalk.bold("\n当前配置:\n"));
574
- console.log(` 模型: ${chalk.cyan(session.model.id)}`);
575
- console.log(` Provider: ${chalk.cyan(session.model.provider)}`);
576
- console.log(` Base URL: ${chalk.cyan(session.model.baseUrl || "(默认)")}`);
577
- console.log(` API Key: ${session.model.apiKey ? chalk.green("已配置") : chalk.red("未设置")}`);
578
- console.log(` Session: ${chalk.cyan(session.sessionManager.shortId)}`);
579
- if (session.model.headers && Object.keys(session.model.headers).length > 0) {
580
- console.log(` Headers: ${chalk.dim(Object.keys(session.model.headers).join(", "))}`);
581
- }
582
- console.log(chalk.bold("\n 所有 Provider:"));
583
- for (const [name, prov] of Object.entries(config.providers)) {
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 用法: /config set model|key|url|add-model (查看 /config help)\n"));
600
- break;
601
- }
602
- if (sub === "help") {
603
- console.log(chalk.bold("\n/config 子命令:\n"));
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
- else {
661
- console.log(chalk.red(`\n 未知字段: ${field}。支持: model, key, url`));
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
- if (sub === "add") {
667
- const what = args[1];
668
- if (what === "model") {
669
- const modelId = args[2];
670
- const provider = args[3];
671
- if (!modelId || !provider) {
672
- console.log(chalk.red("\n 用法: /config add model <model-id> <provider>\n"));
673
- console.log(chalk.dim(" provider 可选: anthropic, openai, openai-compatible, google\n"));
674
- break;
675
- }
676
- if (!config.customModels)
677
- config.customModels = {};
678
- config.customModels[modelId] = {
679
- provider,
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 (sub === "rm") {
694
- const what = args[1];
695
- if (what === "model") {
696
- const modelId = args[2];
697
- if (!modelId) {
698
- console.log(chalk.red("\n 用法: /config rm model <model-id>\n"));
699
- break;
700
- }
701
- if (!config.customModels?.[modelId]) {
702
- console.log(chalk.red(`\n 自定义模型 "${modelId}" 不存在 (内置模型不可删除)\n`));
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.red(`\n 未知子命令: ${sub}`));
716
- 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`));
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
- console.log(` ${chalk.cyan(s.name.padEnd(24))} ${chalk.dim(s.description.slice(0, 60))}`);
728
- 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))}`);
729
810
  }
730
- console.log(chalk.dim("\n 使用 /skill:<名称> 查看技能详情。\n"));
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("欢迎使用千岛湖 Agent 工具 TIL") + " " + chalk.dim(VERSION));
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} 条消息)`)}`