@bgicli/bgicli 2.2.6 → 2.2.8

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/dist/bgi.js CHANGED
@@ -6368,18 +6368,18 @@ function createFileFromPath(path, { mtimeMs, size }, filenameOrOptions, options
6368
6368
  });
6369
6369
  }
6370
6370
  function fileFromPathSync(path, filenameOrOptions, options = {}) {
6371
- const stats = (0, import_fs2.statSync)(path);
6371
+ const stats = (0, import_fs.statSync)(path);
6372
6372
  return createFileFromPath(path, stats, filenameOrOptions, options);
6373
6373
  }
6374
6374
  async function fileFromPath2(path, filenameOrOptions, options) {
6375
- const stats = await import_fs2.promises.stat(path);
6375
+ const stats = await import_fs.promises.stat(path);
6376
6376
  return createFileFromPath(path, stats, filenameOrOptions, options);
6377
6377
  }
6378
- var import_fs2, import_path2, import_node_domexception, __classPrivateFieldSet4, __classPrivateFieldGet5, _FileFromPath_path, _FileFromPath_start, MESSAGE, FileFromPath;
6378
+ var import_fs, import_path, import_node_domexception, __classPrivateFieldSet4, __classPrivateFieldGet5, _FileFromPath_path, _FileFromPath_start, MESSAGE, FileFromPath;
6379
6379
  var init_fileFromPath = __esm({
6380
6380
  "node_modules/formdata-node/lib/esm/fileFromPath.js"() {
6381
- import_fs2 = require("fs");
6382
- import_path2 = require("path");
6381
+ import_fs = require("fs");
6382
+ import_path = require("path");
6383
6383
  import_node_domexception = __toESM(require_node_domexception(), 1);
6384
6384
  init_File();
6385
6385
  init_isPlainObject();
@@ -6402,7 +6402,7 @@ var init_fileFromPath = __esm({
6402
6402
  _FileFromPath_start.set(this, void 0);
6403
6403
  __classPrivateFieldSet4(this, _FileFromPath_path, input.path, "f");
6404
6404
  __classPrivateFieldSet4(this, _FileFromPath_start, input.start || 0, "f");
6405
- this.name = (0, import_path2.basename)(__classPrivateFieldGet5(this, _FileFromPath_path, "f"));
6405
+ this.name = (0, import_path.basename)(__classPrivateFieldGet5(this, _FileFromPath_path, "f"));
6406
6406
  this.size = input.size;
6407
6407
  this.lastModified = input.lastModified;
6408
6408
  }
@@ -6415,12 +6415,12 @@ var init_fileFromPath = __esm({
6415
6415
  });
6416
6416
  }
6417
6417
  async *stream() {
6418
- const { mtimeMs } = await import_fs2.promises.stat(__classPrivateFieldGet5(this, _FileFromPath_path, "f"));
6418
+ const { mtimeMs } = await import_fs.promises.stat(__classPrivateFieldGet5(this, _FileFromPath_path, "f"));
6419
6419
  if (mtimeMs > this.lastModified) {
6420
6420
  throw new import_node_domexception.default(MESSAGE, "NotReadableError");
6421
6421
  }
6422
6422
  if (this.size) {
6423
- yield* (0, import_fs2.createReadStream)(__classPrivateFieldGet5(this, _FileFromPath_path, "f"), {
6423
+ yield* (0, import_fs.createReadStream)(__classPrivateFieldGet5(this, _FileFromPath_path, "f"), {
6424
6424
  start: __classPrivateFieldGet5(this, _FileFromPath_start, "f"),
6425
6425
  end: __classPrivateFieldGet5(this, _FileFromPath_start, "f") + this.size - 1
6426
6426
  });
@@ -6932,105 +6932,10 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
6932
6932
  var source_default = chalk;
6933
6933
 
6934
6934
  // src/index.ts
6935
- var import_fs4 = require("fs");
6936
- var import_path4 = require("path");
6935
+ var import_fs5 = require("fs");
6936
+ var import_path5 = require("path");
6937
6937
  var import_os3 = require("os");
6938
6938
 
6939
- // src/config.ts
6940
- var import_fs = require("fs");
6941
- var import_os = require("os");
6942
- var import_path = require("path");
6943
-
6944
- // src/providers.ts
6945
- var PROVIDERS = {
6946
- bailian: {
6947
- name: "\u767E\u70BC \xB7 \u963F\u91CC\u4E91 (DashScope)",
6948
- baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
6949
- envKey: "DASHSCOPE_API_KEY",
6950
- models: [
6951
- // ── Qwen3.5 (最新旗舰) ────────────────────────────────
6952
- "qwen3.5-397b-a17b",
6953
- "qwen3.5-122b-a10b",
6954
- "qwen3.5-plus",
6955
- "qwen3.5-flash",
6956
- // ── Qwen3 ─────────────────────────────────────────────
6957
- "qwen3-235b-a22b",
6958
- "qwen3-max",
6959
- "qwen3-32b",
6960
- "qwen3-14b",
6961
- "qwen3-8b",
6962
- // ── Qwen3 代码模型 ────────────────────────────────────
6963
- "qwen3-coder-plus",
6964
- "qwen3-coder-flash",
6965
- "qwen3-coder-480b-a35b-instruct",
6966
- // ── 经典 Qwen ─────────────────────────────────────────
6967
- "qwen-max",
6968
- "qwen-plus",
6969
- "qwen-turbo",
6970
- "qwen-long",
6971
- "qwen-flash",
6972
- // ── 推理模型 ──────────────────────────────────────────
6973
- "qwq-plus",
6974
- "deepseek-r1",
6975
- "deepseek-v3",
6976
- "deepseek-v3.2",
6977
- // ── 第三方 (DashScope 聚合) ───────────────────────────
6978
- "kimi-k2.5",
6979
- "kimi-k2-thinking",
6980
- "MiniMax-M2.5",
6981
- "glm-5"
6982
- ],
6983
- defaultModel: "qwen3.5-plus"
6984
- },
6985
- intranet: {
6986
- name: "\u5185\u7F51 Qwen3-235B (172.16.224.137)",
6987
- baseURL: "http://172.16.224.137:1024/v1",
6988
- models: ["Qwen3-235B-A22B"],
6989
- defaultModel: "Qwen3-235B-A22B",
6990
- envKey: ""
6991
- // no auth required
6992
- },
6993
- custom: {
6994
- name: "\u81EA\u5B9A\u4E49 (Custom URL)",
6995
- baseURL: "",
6996
- // filled from config.customUrl at runtime
6997
- models: [],
6998
- // filled from config.customModel at runtime
6999
- defaultModel: "",
7000
- envKey: "CUSTOM_API_KEY"
7001
- }
7002
- };
7003
- var DEFAULT_PROVIDER = "bailian";
7004
-
7005
- // src/config.ts
7006
- var BGI_DIR = (0, import_path.join)((0, import_os.homedir)(), ".bgicli");
7007
- var WORKFLOWS_DIR = (0, import_path.join)(BGI_DIR, "workflows");
7008
- var TOOLS_DIR = (0, import_path.join)(BGI_DIR, "tools");
7009
- var SKILLS_DIR = (0, import_path.join)(BGI_DIR, "skills");
7010
- var CONFIG_FILE = (0, import_path.join)(BGI_DIR, "config.json");
7011
- function ensureDirs() {
7012
- for (const dir of [BGI_DIR, WORKFLOWS_DIR, TOOLS_DIR, SKILLS_DIR]) {
7013
- if (!(0, import_fs.existsSync)(dir)) (0, import_fs.mkdirSync)(dir, { recursive: true });
7014
- }
7015
- }
7016
- function loadConfig() {
7017
- ensureDirs();
7018
- if (!(0, import_fs.existsSync)(CONFIG_FILE)) {
7019
- const def = {
7020
- provider: DEFAULT_PROVIDER,
7021
- model: PROVIDERS[DEFAULT_PROVIDER].defaultModel,
7022
- apiKeys: {}
7023
- };
7024
- saveConfig(def);
7025
- return def;
7026
- }
7027
- return JSON.parse((0, import_fs.readFileSync)(CONFIG_FILE, "utf8"));
7028
- }
7029
- function saveConfig(cfg) {
7030
- ensureDirs();
7031
- (0, import_fs.writeFileSync)(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf8");
7032
- }
7033
-
7034
6939
  // node_modules/openai/internal/qs/formats.mjs
7035
6940
  var default_format = "RFC3986";
7036
6941
  var formatters = {
@@ -13627,11 +13532,108 @@ OpenAI.Containers = Containers;
13627
13532
  OpenAI.ContainerListResponsesPage = ContainerListResponsesPage;
13628
13533
  var openai_default = OpenAI;
13629
13534
 
13535
+ // src/config.ts
13536
+ var import_fs2 = require("fs");
13537
+ var import_os = require("os");
13538
+ var import_path2 = require("path");
13539
+
13540
+ // src/providers.ts
13541
+ var PROVIDERS = {
13542
+ bailian: {
13543
+ name: "\u767E\u70BC \xB7 \u963F\u91CC\u4E91 (DashScope)",
13544
+ baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
13545
+ envKey: "DASHSCOPE_API_KEY",
13546
+ models: [
13547
+ // ── Qwen3.5 (最新旗舰) ────────────────────────────────
13548
+ "qwen3.5-397b-a17b",
13549
+ "qwen3.5-122b-a10b",
13550
+ "qwen3.5-plus",
13551
+ "qwen3.5-flash",
13552
+ // ── Qwen3 ─────────────────────────────────────────────
13553
+ "qwen3-235b-a22b",
13554
+ "qwen3-max",
13555
+ "qwen3-32b",
13556
+ "qwen3-14b",
13557
+ "qwen3-8b",
13558
+ // ── Qwen3 代码模型 ────────────────────────────────────
13559
+ "qwen3-coder-plus",
13560
+ "qwen3-coder-flash",
13561
+ "qwen3-coder-480b-a35b-instruct",
13562
+ // ── 经典 Qwen ─────────────────────────────────────────
13563
+ "qwen-max",
13564
+ "qwen-plus",
13565
+ "qwen-turbo",
13566
+ "qwen-long",
13567
+ "qwen-flash",
13568
+ // ── 推理模型 ──────────────────────────────────────────
13569
+ "qwq-plus",
13570
+ "deepseek-r1",
13571
+ "deepseek-v3",
13572
+ "deepseek-v3.2",
13573
+ // ── 第三方 (DashScope 聚合) ───────────────────────────
13574
+ "kimi-k2.5",
13575
+ "kimi-k2-thinking",
13576
+ "MiniMax-M2.5",
13577
+ "glm-5"
13578
+ ],
13579
+ defaultModel: "qwen3.5-plus"
13580
+ },
13581
+ intranet: {
13582
+ name: "\u5185\u7F51 Qwen3-235B (172.16.224.137)",
13583
+ baseURL: "http://172.16.224.137:1024/v1",
13584
+ models: ["Qwen3-235B-A22B"],
13585
+ defaultModel: "Qwen3-235B-A22B",
13586
+ envKey: ""
13587
+ // no auth required
13588
+ },
13589
+ custom: {
13590
+ name: "\u81EA\u5B9A\u4E49 (Custom URL)",
13591
+ baseURL: "",
13592
+ // filled from config.customUrl at runtime
13593
+ models: [],
13594
+ // filled from config.customModel at runtime
13595
+ defaultModel: "",
13596
+ envKey: "CUSTOM_API_KEY"
13597
+ }
13598
+ };
13599
+ var DEFAULT_PROVIDER = "bailian";
13600
+
13601
+ // src/config.ts
13602
+ var BGI_DIR = (0, import_path2.join)((0, import_os.homedir)(), ".bgicli");
13603
+ var WORKFLOWS_DIR = (0, import_path2.join)(BGI_DIR, "workflows");
13604
+ var TOOLS_DIR = (0, import_path2.join)(BGI_DIR, "tools");
13605
+ var SKILLS_DIR = (0, import_path2.join)(BGI_DIR, "skills");
13606
+ var CONFIG_FILE = (0, import_path2.join)(BGI_DIR, "config.json");
13607
+ function ensureDirs() {
13608
+ for (const dir of [BGI_DIR, WORKFLOWS_DIR, TOOLS_DIR, SKILLS_DIR]) {
13609
+ if (!(0, import_fs2.existsSync)(dir)) (0, import_fs2.mkdirSync)(dir, { recursive: true });
13610
+ }
13611
+ }
13612
+ function loadConfig() {
13613
+ ensureDirs();
13614
+ if (!(0, import_fs2.existsSync)(CONFIG_FILE)) {
13615
+ const def = {
13616
+ provider: DEFAULT_PROVIDER,
13617
+ model: PROVIDERS[DEFAULT_PROVIDER].defaultModel,
13618
+ apiKeys: {}
13619
+ };
13620
+ saveConfig(def);
13621
+ return def;
13622
+ }
13623
+ return JSON.parse((0, import_fs2.readFileSync)(CONFIG_FILE, "utf8"));
13624
+ }
13625
+ function saveConfig(cfg) {
13626
+ ensureDirs();
13627
+ (0, import_fs2.writeFileSync)(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf8");
13628
+ }
13629
+
13630
13630
  // src/tools.ts
13631
13631
  var import_child_process = require("child_process");
13632
13632
  var import_fs3 = require("fs");
13633
13633
  var import_path3 = require("path");
13634
13634
  var import_os2 = require("os");
13635
+ var import_https = require("https");
13636
+ var import_http = require("http");
13635
13637
  var TOOL_DEFINITIONS = [
13636
13638
  {
13637
13639
  type: "function",
@@ -13651,7 +13653,7 @@ var TOOL_DEFINITIONS = [
13651
13653
  },
13652
13654
  timeout_ms: {
13653
13655
  type: "number",
13654
- description: "Timeout in milliseconds (default 30000, max 300000 for long jobs)"
13656
+ description: "Timeout in milliseconds (default 300000 / 5 min, max 1800000 / 30 min for long jobs like STAR alignment)"
13655
13657
  }
13656
13658
  },
13657
13659
  required: ["command"]
@@ -13669,7 +13671,7 @@ var TOOL_DEFINITIONS = [
13669
13671
  path: { type: "string", description: "Absolute or relative file path" },
13670
13672
  max_lines: {
13671
13673
  type: "number",
13672
- description: "Maximum number of lines to return (default: 200)"
13674
+ description: "Maximum number of lines to return (default: 500)"
13673
13675
  },
13674
13676
  offset: {
13675
13677
  type: "number",
@@ -13729,21 +13731,43 @@ var TOOL_DEFINITIONS = [
13729
13731
  required: ["pattern"]
13730
13732
  }
13731
13733
  }
13734
+ },
13735
+ {
13736
+ type: "function",
13737
+ function: {
13738
+ name: "fetch_geo",
13739
+ description: "Query NCBI GEO database by accession number (GSE, GDS, GPL, GSM). Returns dataset metadata, sample info, organism, platform, and download links. Use this BEFORE asking the user to manually download data \u2014 always try fetch_geo first when a GEO accession is mentioned.",
13740
+ parameters: {
13741
+ type: "object",
13742
+ properties: {
13743
+ accession: {
13744
+ type: "string",
13745
+ description: 'GEO accession number, e.g. "GSE12345", "GDS1234", "GPL570"'
13746
+ },
13747
+ include_samples: {
13748
+ type: "boolean",
13749
+ description: "Whether to include individual sample (GSM) metadata (default: false, set true for small datasets)"
13750
+ }
13751
+ },
13752
+ required: ["accession"]
13753
+ }
13754
+ }
13732
13755
  }
13733
13756
  ];
13734
- function executeTool(name, args) {
13757
+ async function executeTool(name, args, onStream) {
13735
13758
  try {
13736
13759
  switch (name) {
13737
13760
  case "bash":
13738
- return toolBash(
13761
+ return await toolBash(
13739
13762
  args["command"],
13740
13763
  args["workdir"],
13741
- args["timeout_ms"] ?? 3e4
13764
+ args["timeout_ms"] ?? 3e5,
13765
+ onStream
13742
13766
  );
13743
13767
  case "read_file":
13744
13768
  return toolReadFile(
13745
13769
  args["path"],
13746
- args["max_lines"] ?? 200,
13770
+ args["max_lines"] ?? 500,
13747
13771
  args["offset"] ?? 0
13748
13772
  );
13749
13773
  case "write_file":
@@ -13751,10 +13775,15 @@ function executeTool(name, args) {
13751
13775
  case "list_dir":
13752
13776
  return toolListDir(args["path"]);
13753
13777
  case "search_files":
13754
- return toolSearchFiles(
13778
+ return await toolSearchFiles(
13755
13779
  args["pattern"],
13756
13780
  args["path"] ?? process.cwd()
13757
13781
  );
13782
+ case "fetch_geo":
13783
+ return await toolFetchGeo(
13784
+ args["accession"],
13785
+ args["include_samples"] ?? false
13786
+ );
13758
13787
  default:
13759
13788
  return { output: "", error: `Unknown tool: ${name}` };
13760
13789
  }
@@ -13775,23 +13804,76 @@ function decodeBuffer(buf) {
13775
13804
  }
13776
13805
  }
13777
13806
  }
13778
- function toolBash(command, workdir, timeoutMs = 3e4) {
13779
- try {
13780
- const buf = (0, import_child_process.execSync)(command, {
13807
+ var DANGEROUS_PATTERNS = [
13808
+ { pattern: /rm\s+-rf\s+\/(?!\S)/, reason: "\u5220\u9664\u6839\u76EE\u5F55 (rm -rf /)" },
13809
+ { pattern: /rm\s+-rf\s+~(?!\S)/, reason: "\u5220\u9664 home \u76EE\u5F55 (rm -rf ~)" },
13810
+ { pattern: /rm\s+-rf\s+\$HOME(?!\S)/, reason: "\u5220\u9664 $HOME \u76EE\u5F55" },
13811
+ { pattern: /dd\s+if=\/dev\/(?:zero|random|urandom)\s+of=\/dev\//, reason: "\u8986\u5199\u78C1\u76D8\u8BBE\u5907 (dd)" },
13812
+ { pattern: /mkfs\b/, reason: "\u683C\u5F0F\u5316\u6587\u4EF6\u7CFB\u7EDF (mkfs)" },
13813
+ { pattern: />\s*\/dev\/sd[a-z]/, reason: "\u76F4\u63A5\u5199\u5165\u78C1\u76D8\u8BBE\u5907" },
13814
+ { pattern: /chmod\s+-R\s+777\s+\/(?!\S)/, reason: "\u9012\u5F52\u4FEE\u6539\u6839\u76EE\u5F55\u6743\u9650" },
13815
+ { pattern: /:\(\)\s*\{.*\}.*:/, reason: "Fork bomb \u68C0\u6D4B" }
13816
+ ];
13817
+ function checkDangerousCommand(command) {
13818
+ for (const { pattern, reason } of DANGEROUS_PATTERNS) {
13819
+ if (pattern.test(command)) return reason;
13820
+ }
13821
+ return null;
13822
+ }
13823
+ async function toolBash(command, workdir, timeoutMs = 3e5, onStream) {
13824
+ const danger = checkDangerousCommand(command);
13825
+ if (danger) {
13826
+ return {
13827
+ output: "",
13828
+ error: `\u26A0\uFE0F \u5B89\u5168\u62E6\u622A\uFF1A\u68C0\u6D4B\u5230\u5371\u9669\u547D\u4EE4\uFF08${danger}\uFF09\u3002
13829
+ \u547D\u4EE4\u5DF2\u88AB\u963B\u6B62\uFF0C\u8BF7\u786E\u8BA4\u4F60\u7684\u610F\u56FE\u540E\u624B\u52A8\u6267\u884C\u3002
13830
+ \u88AB\u62E6\u622A\u7684\u547D\u4EE4: ${command}`
13831
+ };
13832
+ }
13833
+ return new Promise((resolve3) => {
13834
+ const isWin = process.platform === "win32";
13835
+ const child = (0, import_child_process.spawn)(isWin ? "cmd" : "/bin/sh", isWin ? ["/c", command] : ["-c", command], {
13781
13836
  cwd: workdir ?? process.cwd(),
13782
- timeout: timeoutMs,
13783
- encoding: "buffer",
13784
- stdio: ["pipe", "pipe", "pipe"],
13785
- maxBuffer: 10 * 1024 * 1024,
13786
- env: { ...process.env, PYTHONIOENCODING: "utf-8" }
13837
+ env: { ...process.env, PYTHONIOENCODING: "utf-8" },
13838
+ stdio: ["pipe", "pipe", "pipe"]
13787
13839
  });
13788
- return { output: decodeBuffer(buf).trim() };
13789
- } catch (err) {
13790
- const e2 = err;
13791
- const out = (decodeBuffer(e2.stdout) + "\n" + decodeBuffer(e2.stderr)).trim();
13792
- const cmdLine = (e2.message ?? "Command failed").split("\n")[0];
13793
- return { output: out, error: cmdLine };
13794
- }
13840
+ const outChunks = [];
13841
+ const errChunks = [];
13842
+ const MAX = 10 * 1024 * 1024;
13843
+ let total = 0;
13844
+ child.stdout?.on("data", (c2) => {
13845
+ if ((total += c2.length) <= MAX) {
13846
+ outChunks.push(c2);
13847
+ if (onStream) onStream(decodeBuffer(c2));
13848
+ }
13849
+ });
13850
+ child.stderr?.on("data", (c2) => {
13851
+ if ((total += c2.length) <= MAX) {
13852
+ errChunks.push(c2);
13853
+ if (onStream) onStream(decodeBuffer(c2));
13854
+ }
13855
+ });
13856
+ let timedOut = false;
13857
+ const timer = setTimeout(() => {
13858
+ timedOut = true;
13859
+ child.kill();
13860
+ }, timeoutMs);
13861
+ child.on("close", (code) => {
13862
+ clearTimeout(timer);
13863
+ const out = (decodeBuffer(Buffer.concat(outChunks)) + "\n" + decodeBuffer(Buffer.concat(errChunks))).trim();
13864
+ if (timedOut) {
13865
+ resolve3({ output: out, error: `Command timed out after ${timeoutMs / 1e3}s` });
13866
+ } else if (code !== 0) {
13867
+ resolve3({ output: out, error: `Command failed (exit ${code})` });
13868
+ } else {
13869
+ resolve3({ output: out });
13870
+ }
13871
+ });
13872
+ child.on("error", (err) => {
13873
+ clearTimeout(timer);
13874
+ resolve3({ output: "", error: err.message });
13875
+ });
13876
+ });
13795
13877
  }
13796
13878
  function toolReadFile(path, maxLines, offset) {
13797
13879
  const resolved = (0, import_path3.resolve)(path.replace(/^~/, (0, import_os2.homedir)()));
@@ -13820,18 +13902,127 @@ function toolListDir(path) {
13820
13902
  });
13821
13903
  return { output: entries.join("\n") };
13822
13904
  }
13823
- function toolSearchFiles(pattern, rootPath) {
13905
+ async function toolSearchFiles(pattern, rootPath) {
13824
13906
  const resolved = (0, import_path3.resolve)(rootPath.replace(/^~/, (0, import_os2.homedir)()));
13825
13907
  const isWin = process.platform === "win32";
13826
- let command;
13827
- if (isWin) {
13828
- command = `dir /s /b "${resolved}\\${pattern}" 2>nul`;
13829
- } else {
13830
- const name = pattern.includes("/") ? pattern : `"${pattern}"`;
13831
- command = `find "${resolved}" -name ${name} 2>/dev/null | head -50`;
13832
- }
13908
+ const command = isWin ? `dir /s /b "${resolved}\\${pattern}" 2>nul` : `find "${resolved}" -name ${pattern.includes("/") ? pattern : `"${pattern}"`} 2>/dev/null | head -50`;
13833
13909
  return toolBash(command, resolved, 1e4);
13834
13910
  }
13911
+ function httpFetch(url, timeoutMs = 15e3) {
13912
+ return new Promise((resolve3, reject) => {
13913
+ const getter = url.startsWith("https") ? import_https.get : import_http.get;
13914
+ const req = getter(url, { headers: { "User-Agent": "BGI-CLI/1.0" } }, (res) => {
13915
+ if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
13916
+ httpFetch(res.headers.location, timeoutMs).then(resolve3).catch(reject);
13917
+ return;
13918
+ }
13919
+ if (res.statusCode && res.statusCode >= 400) {
13920
+ reject(new Error(`HTTP ${res.statusCode}`));
13921
+ return;
13922
+ }
13923
+ const chunks = [];
13924
+ res.on("data", (c2) => chunks.push(c2));
13925
+ res.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
13926
+ res.on("error", reject);
13927
+ });
13928
+ req.setTimeout(timeoutMs, () => {
13929
+ req.destroy();
13930
+ reject(new Error("Request timed out"));
13931
+ });
13932
+ req.on("error", reject);
13933
+ });
13934
+ }
13935
+ async function toolFetchGeo(accession, includeSamples) {
13936
+ const acc = accession.trim().toUpperCase();
13937
+ if (!/^(GSE|GDS|GPL|GSM)\d+$/.test(acc)) {
13938
+ return {
13939
+ output: "",
13940
+ error: `\u65E0\u6548\u7684 GEO \u7F16\u53F7: "${acc}"\u3002\u652F\u6301\u683C\u5F0F: GSE12345, GDS1234, GPL570, GSM123456`
13941
+ };
13942
+ }
13943
+ const accType = acc.slice(0, 3);
13944
+ try {
13945
+ const searchUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=gds&term=${acc}[Accession]&retmode=json&retmax=1`;
13946
+ const searchRaw = await httpFetch(searchUrl);
13947
+ const searchJson = JSON.parse(searchRaw);
13948
+ const idList = searchJson.esearchresult?.idlist ?? [];
13949
+ if (idList.length === 0) {
13950
+ return { output: "", error: `\u672A\u627E\u5230 GEO \u8BB0\u5F55: ${acc}\u3002\u8BF7\u786E\u8BA4\u7F16\u53F7\u662F\u5426\u6B63\u786E\u3002` };
13951
+ }
13952
+ const uid = idList[0];
13953
+ const summaryUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=gds&id=${uid}&retmode=json`;
13954
+ const summaryRaw = await httpFetch(summaryUrl);
13955
+ const summaryJson = JSON.parse(summaryRaw);
13956
+ const rec = summaryJson.result?.[uid];
13957
+ if (!rec) {
13958
+ return { output: "", error: `\u65E0\u6CD5\u83B7\u53D6 ${acc} \u7684\u8BE6\u7EC6\u4FE1\u606F` };
13959
+ }
13960
+ const lines = [];
13961
+ lines.push(`=== GEO \u6570\u636E\u96C6: ${acc} ===`);
13962
+ lines.push("");
13963
+ if (rec.title) lines.push(`\u6807\u9898: ${rec.title}`);
13964
+ if (rec.taxon || rec.organism)
13965
+ lines.push(`\u7269\u79CD: ${rec.taxon ?? rec.organism}`);
13966
+ if (rec.gdstype || rec.ptechtype)
13967
+ lines.push(`\u7C7B\u578B: ${rec.gdstype ?? rec.ptechtype}`);
13968
+ if (rec.gpl) lines.push(`\u5E73\u53F0: GPL${rec.gpl}`);
13969
+ if (rec.n_samples) lines.push(`\u6837\u672C\u6570: ${rec.n_samples}`);
13970
+ if (rec.summary) {
13971
+ const shortSummary = rec.summary.length > 500 ? rec.summary.slice(0, 500) + "..." : rec.summary;
13972
+ lines.push("");
13973
+ lines.push(`\u6458\u8981:
13974
+ ${shortSummary}`);
13975
+ }
13976
+ lines.push("");
13977
+ lines.push("=== \u4E0B\u8F7D\u94FE\u63A5 ===");
13978
+ lines.push(`GEO \u9875\u9762: https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc=${acc}`);
13979
+ if (accType === "GSE") {
13980
+ lines.push(`\u77E9\u9635\u6587\u4EF6: https://ftp.ncbi.nlm.nih.gov/geo/series/${acc.slice(0, -3)}nnn/${acc}/matrix/`);
13981
+ lines.push(`\u539F\u59CB\u6570\u636E: https://ftp.ncbi.nlm.nih.gov/geo/series/${acc.slice(0, -3)}nnn/${acc}/suppl/`);
13982
+ lines.push("");
13983
+ lines.push("=== R \u4E0B\u8F7D\u4EE3\u7801 ===");
13984
+ lines.push("```r");
13985
+ lines.push("# \u65B9\u6CD51: GEOquery\uFF08\u63A8\u8350\uFF0C\u81EA\u52A8\u89E3\u6790\uFF09");
13986
+ lines.push('if (!require("GEOquery")) BiocManager::install("GEOquery")');
13987
+ lines.push(`gse <- getGEO("${acc}", GSEMatrix = TRUE, getGPL = FALSE)`);
13988
+ lines.push("expr_matrix <- exprs(gse[[1]]) # \u8868\u8FBE\u77E9\u9635");
13989
+ lines.push("pheno_data <- pData(gse[[1]]) # \u6837\u672C\u5143\u6570\u636E");
13990
+ lines.push("");
13991
+ lines.push("# \u65B9\u6CD52: \u76F4\u63A5\u4E0B\u8F7D\u77E9\u9635\u6587\u4EF6");
13992
+ lines.push(`url <- "https://ftp.ncbi.nlm.nih.gov/geo/series/${acc.slice(0, -3)}nnn/${acc}/matrix/${acc}_series_matrix.txt.gz"`);
13993
+ lines.push('download.file(url, destfile = "series_matrix.txt.gz")');
13994
+ lines.push("```");
13995
+ lines.push("");
13996
+ lines.push("=== Python \u4E0B\u8F7D\u4EE3\u7801 ===");
13997
+ lines.push("```python");
13998
+ lines.push("import GEOparse");
13999
+ lines.push(`gse = GEOparse.get_GEO(geo="${acc}", destdir="./data")`);
14000
+ lines.push("# gse.gsms \u2014 \u6837\u672C\u5B57\u5178");
14001
+ lines.push("# gse.gpls \u2014 \u5E73\u53F0\u4FE1\u606F");
14002
+ lines.push("```");
14003
+ }
14004
+ if (rec.suppfile) {
14005
+ lines.push("");
14006
+ lines.push(`\u8865\u5145\u6587\u4EF6: ${rec.suppfile}`);
14007
+ }
14008
+ if (includeSamples && rec.samples && rec.samples.length > 0) {
14009
+ lines.push("");
14010
+ lines.push(`=== \u6837\u672C\u5217\u8868 (${rec.samples.length} \u4E2A) ===`);
14011
+ const showSamples = rec.samples.slice(0, 20);
14012
+ showSamples.forEach((s2) => lines.push(` ${s2.accession}: ${s2.title}`));
14013
+ if (rec.samples.length > 20) {
14014
+ lines.push(` ... \u8FD8\u6709 ${rec.samples.length - 20} \u4E2A\u6837\u672C`);
14015
+ }
14016
+ }
14017
+ return { output: lines.join("\n") };
14018
+ } catch (err) {
14019
+ const msg = err instanceof Error ? err.message : String(err);
14020
+ return {
14021
+ output: `GEO \u9875\u9762: https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc=${acc}`,
14022
+ error: `\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25 (${msg})\u3002\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\uFF0C\u6216\u76F4\u63A5\u8BBF\u95EE\u4E0A\u65B9\u94FE\u63A5\u3002`
14023
+ };
14024
+ }
14025
+ }
13835
14026
 
13836
14027
  // src/chat.ts
13837
14028
  async function chat(messages, config, systemPrompt) {
@@ -13856,6 +14047,7 @@ async function chat(messages, config, systemPrompt) {
13856
14047
  async function streamLoop(client, messages, model) {
13857
14048
  let finalText = "";
13858
14049
  for (let round = 0; round < 20; round++) {
14050
+ messages = deduplicateSkillInjections(trimToolOutputs(messages));
13859
14051
  const { text, toolCalls, finishReason } = await streamOnce(client, messages, model);
13860
14052
  if (text) finalText = text;
13861
14053
  if (finishReason === "tool_calls" && toolCalls.length > 0) {
@@ -13869,17 +14061,66 @@ async function streamLoop(client, messages, model) {
13869
14061
  function: { name: tc.name, arguments: tc.args }
13870
14062
  }))
13871
14063
  });
14064
+ const SPIN_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
13872
14065
  for (const tc of toolCalls) {
13873
14066
  const args = parseArgs(tc.args);
13874
- process.stdout.write(source_default.dim(`
13875
- [\u5DE5\u5177: ${tc.name}(${summarizeArgs(args)})]
13876
- `));
13877
- const result = executeTool(tc.name, args);
14067
+ const isBash = tc.name === "bash";
14068
+ const label = source_default.dim(`[\u5DE5\u5177: ${tc.name}(${summarizeArgs(args)})]`);
14069
+ const t0 = Date.now();
14070
+ process.stdout.write(`
14071
+ ${label}
14072
+ `);
14073
+ let streamedLines = 0;
14074
+ let lastLineWasEmpty = false;
14075
+ const MAX_STREAM_LINES = 200;
14076
+ let spin = null;
14077
+ let frame = 0;
14078
+ const onStream = isBash ? (chunk) => {
14079
+ if (streamedLines >= MAX_STREAM_LINES) return;
14080
+ const lines = chunk.split("\n");
14081
+ for (let i2 = 0; i2 < lines.length; i2++) {
14082
+ const line = lines[i2];
14083
+ if (line.trim() === "") {
14084
+ if (lastLineWasEmpty) continue;
14085
+ lastLineWasEmpty = true;
14086
+ } else {
14087
+ lastLineWasEmpty = false;
14088
+ }
14089
+ if (i2 < lines.length - 1 || line.length > 0) {
14090
+ process.stdout.write(source_default.dim(" \u2502 ") + line + (i2 < lines.length - 1 ? "\n" : ""));
14091
+ streamedLines++;
14092
+ if (streamedLines >= MAX_STREAM_LINES) {
14093
+ process.stdout.write(source_default.dim("\n \u2502 ... (\u8F93\u51FA\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD)\n"));
14094
+ break;
14095
+ }
14096
+ }
14097
+ }
14098
+ } : void 0;
14099
+ if (!isBash) {
14100
+ spin = setInterval(() => {
14101
+ const secs = ((Date.now() - t0) / 1e3).toFixed(1);
14102
+ process.stdout.write(
14103
+ `\r ${source_default.cyan(SPIN_FRAMES[frame++ % SPIN_FRAMES.length])} ${source_default.dim(secs + "s")}`
14104
+ );
14105
+ }, 80);
14106
+ }
14107
+ const result = await executeTool(tc.name, args, onStream);
14108
+ if (spin) {
14109
+ clearInterval(spin);
14110
+ process.stdout.write("\r\x1B[2K");
14111
+ }
14112
+ const elapsed = ((Date.now() - t0) / 1e3).toFixed(1);
14113
+ const doneIcon = result.error ? source_default.yellow("\u2717") : source_default.green("\u2713");
14114
+ if (isBash && streamedLines > 0) {
14115
+ process.stdout.write("\n");
14116
+ }
14117
+ process.stdout.write(` ${doneIcon} ${source_default.dim("\u5B8C\u6210 " + elapsed + "s")}
14118
+ `);
13878
14119
  if (result.error) {
13879
14120
  process.stdout.write(source_default.yellow(` \u26A0 ${result.error}
13880
14121
  `));
13881
14122
  }
13882
- if (result.output) {
14123
+ if (!isBash && result.output) {
13883
14124
  const preview = result.output.split("\n").slice(0, 3).join("\n");
13884
14125
  const more = result.output.split("\n").length > 3;
13885
14126
  process.stdout.write(source_default.dim(` ${preview}${more ? "\n ..." : ""}
@@ -13944,6 +14185,52 @@ async function streamOnce(client, messages, model) {
13944
14185
  finishReason
13945
14186
  };
13946
14187
  }
14188
+ function estimateTokens(messages) {
14189
+ const chars = messages.reduce((n2, m2) => {
14190
+ const content = m2.content;
14191
+ if (typeof content === "string") return n2 + content.length;
14192
+ if (Array.isArray(content)) {
14193
+ return n2 + content.reduce((s2, c2) => s2 + (typeof c2 === "object" && "text" in c2 ? c2.text.length : 0), 0);
14194
+ }
14195
+ return n2;
14196
+ }, 0);
14197
+ return Math.round(chars / 3.5);
14198
+ }
14199
+ var MAX_TOOL_OUTPUT_CHARS = 8e3;
14200
+ function trimToolOutputs(messages) {
14201
+ return messages.map((m2) => {
14202
+ if (m2.role !== "tool") return m2;
14203
+ const content = typeof m2.content === "string" ? m2.content : "";
14204
+ if (content.length <= MAX_TOOL_OUTPUT_CHARS) return m2;
14205
+ const head = content.slice(0, MAX_TOOL_OUTPUT_CHARS / 2);
14206
+ const tail = content.slice(-MAX_TOOL_OUTPUT_CHARS / 2);
14207
+ const trimmed = `${head}
14208
+
14209
+ ... [\u8F93\u51FA\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD ${content.length - MAX_TOOL_OUTPUT_CHARS} \u5B57\u7B26] ...
14210
+
14211
+ ${tail}`;
14212
+ return { ...m2, content: trimmed };
14213
+ });
14214
+ }
14215
+ function deduplicateSkillInjections(messages) {
14216
+ const SKILL_MARKER = "[Skill \u5DF2\u52A0\u8F7D:";
14217
+ const seenSkills = /* @__PURE__ */ new Set();
14218
+ const result = [];
14219
+ for (let i2 = messages.length - 1; i2 >= 0; i2--) {
14220
+ const m2 = messages[i2];
14221
+ const content = typeof m2.content === "string" ? m2.content : "";
14222
+ if (m2.role === "user" && content.startsWith(SKILL_MARKER)) {
14223
+ const idMatch = content.match(/\[Skill 已加载: ([^\]]+)\]/);
14224
+ const skillId = idMatch?.[1];
14225
+ if (skillId) {
14226
+ if (seenSkills.has(skillId)) continue;
14227
+ seenSkills.add(skillId);
14228
+ }
14229
+ }
14230
+ result.unshift(m2);
14231
+ }
14232
+ return result;
14233
+ }
13947
14234
  async function compactMessages(messages, config) {
13948
14235
  const prov = PROVIDERS[config.provider];
13949
14236
  if (!prov) throw new Error(`Unknown provider: ${config.provider}`);
@@ -14010,6 +14297,7 @@ You have access to these tools:
14010
14297
  - **write_file**: Create or update files
14011
14298
  - **list_dir**: List directory contents
14012
14299
  - **search_files**: Find files by pattern (glob)
14300
+ - **fetch_geo**: Query NCBI GEO database by accession (GSE/GDS/GPL/GSM). Returns metadata, sample info, organism, platform, and ready-to-use R/Python download code. **Always call this first when the user mentions a GEO accession number \u2014 never ask them to download manually.**
14013
14301
 
14014
14302
  **MANDATORY WORKFLOW**: When the user gives you a bioinformatics task:
14015
14303
  1. Check if a matching pre-built workflow exists (see Workflow Library below)
@@ -14062,6 +14350,7 @@ cat ${WORKFLOWS_DIR}/<workflow-id>/SKILL.md
14062
14350
 
14063
14351
  | ID | Use When |
14064
14352
  |----|----------|
14353
+ | \`survival-analysis-clinical\` | \u751F\u5B58\u5206\u6790\uFF1AKM \u66F2\u7EBF\u3001log-rank \u68C0\u9A8C\u3001Cox \u56DE\u5F52\u3001\u7ADE\u4E89\u98CE\u9669\uFF08OS/PFS/DFS\uFF09 |
14065
14354
  | \`clinicaltrials-landscape\` | ClinicalTrials.gov \u6570\u636E\u5206\u6790 |
14066
14355
  | \`literature-preclinical\` | \u4E34\u5E8A\u524D\u6587\u732E\u7CFB\u7EDF\u63D0\u53D6\u4E0E\u7EFC\u5408 |
14067
14356
  | \`experimental-design-statistics\` | \u7EDF\u8BA1\u68C0\u9A8C\u9009\u62E9\u3001\u6837\u672C\u91CF\u8BA1\u7B97\u3001\u968F\u673A\u5316\u65B9\u6848 |
@@ -14145,15 +14434,71 @@ samtools --version 2>&1 | head -1
14145
14434
 
14146
14435
  ---
14147
14436
 
14148
- ## Script Execution Rules
14149
-
14150
- \u{1F6A8} **\u5173\u952E\u89C4\u5219\uFF1A**
14151
- 1. **\u4F18\u5148\u4F7F\u7528\u5DE5\u4F5C\u6D41\u811A\u672C**\uFF0C\u4E0D\u8981\u4ECE\u96F6\u5199\u4EE3\u7801
14152
- 2. **\u811A\u672C\u5931\u8D25\u5904\u7406\u987A\u5E8F**: \u4FEE\u590D\u5E76\u91CD\u8BD5 \u2192 \u4FEE\u6539\u811A\u672C \u2192 \u9002\u914D\u65B9\u6848 \u2192 \u6700\u540E\u624D\u4ECE\u5934\u5199
14153
- 3. **\u4F7F\u7528\u76F8\u5BF9\u8DEF\u5F84**\uFF1A\u5728\u5DE5\u4F5C\u6D41\u76EE\u5F55\u5185\u7528 \`source("scripts/xxx.R")\` \u800C\u975E\u7EDD\u5BF9\u8DEF\u5F84
14154
- 4. **\u9A8C\u8BC1\u6D88\u606F**\uFF1A\u6BCF\u6B65\u5B8C\u6210\u5E94\u770B\u5230 "\u2713" \u786E\u8BA4\u6D88\u606F\uFF1B\u770B\u4E0D\u5230\u8BF4\u660E\u6CA1\u7528\u811A\u672C
14437
+ ## Data Integrity Rules\uFF08\u5206\u6790\u524D\u5FC5\u987B\u6267\u884C\uFF09
14155
14438
 
14156
- ---
14439
+ \u{1F52C} **\u5728\u4EFB\u4F55\u7EDF\u8BA1\u5206\u6790\u5F00\u59CB\u524D\uFF0C\u5FC5\u987B\u5B8C\u6210\u4EE5\u4E0B\u68C0\u67E5\uFF1A**
14440
+
14441
+ ### 1. \u6837\u672C ID \u4E00\u81F4\u6027
14442
+ \`\`\`r
14443
+ # R: \u68C0\u67E5 count \u77E9\u9635\u5217\u540D\u4E0E metadata \u884C\u540D\u662F\u5426\u5B8C\u5168\u4E00\u81F4
14444
+ stopifnot(all(colnames(counts) == rownames(metadata)))
14445
+ # \u5982\u679C\u4E0D\u4E00\u81F4\uFF0C\u5148\u5BF9\u9F50\u518D\u5206\u6790\uFF1A
14446
+ metadata <- metadata[colnames(counts), , drop = FALSE]
14447
+ \`\`\`
14448
+ \`\`\`python
14449
+ # Python: \u68C0\u67E5 AnnData obs_names \u4E0E metadata index \u662F\u5426\u4E00\u81F4
14450
+ assert list(adata.obs_names) == list(metadata.index), "\u6837\u672C ID \u4E0D\u5339\u914D\uFF01"
14451
+ \`\`\`
14452
+
14453
+ ### 2. \u91CD\u590D\u884C/\u5217\u540D\u68C0\u6D4B
14454
+ \`\`\`r
14455
+ # \u68C0\u67E5\u91CD\u590D\u57FA\u56E0\u540D\uFF08\u884C\u540D\uFF09
14456
+ if (any(duplicated(rownames(counts)))) {
14457
+ warning("\u53D1\u73B0\u91CD\u590D\u57FA\u56E0\u540D\uFF0C\u5C06\u805A\u5408\u91CD\u590D\u884C\uFF08\u53D6\u5747\u503C\uFF09")
14458
+ counts <- aggregate(counts, by = list(rownames(counts)), FUN = mean)
14459
+ }
14460
+ \`\`\`
14461
+
14462
+ ### 3. \u7F3A\u5931\u503C\u62A5\u544A
14463
+ \`\`\`r
14464
+ na_pct <- sum(is.na(counts)) / prod(dim(counts)) * 100
14465
+ message(sprintf("\u7F3A\u5931\u503C\u6BD4\u4F8B: %.2f%%", na_pct))
14466
+ if (na_pct > 5) warning("\u7F3A\u5931\u503C\u8D85\u8FC7 5%\uFF0C\u8BF7\u68C0\u67E5\u6570\u636E\u8D28\u91CF")
14467
+ \`\`\`
14468
+
14469
+ ### 4. \u5DEE\u5F02\u8868\u8FBE\u5206\u6790\uFF1A\u5FC5\u987B\u4F7F\u7528 padj\uFF0C\u7981\u6B62\u4F7F\u7528\u539F\u59CB pvalue
14470
+ \`\`\`r
14471
+ # \u2705 \u6B63\u786E\uFF1A\u4F7F\u7528 FDR \u6821\u6B63\u540E\u7684 p \u503C
14472
+ sig_genes <- res[!is.na(res$padj) & res$padj <= 0.05 & abs(res$log2FoldChange) >= 1, ]
14473
+
14474
+ # \u274C \u9519\u8BEF\uFF1A\u4E0D\u8981\u7528\u539F\u59CB pvalue \u7B5B\u9009 DEG
14475
+ # sig_genes <- res[res$pvalue < 0.05, ] # \u8FD9\u662F\u9519\u7684\uFF01
14476
+ \`\`\`
14477
+
14478
+ **\u6807\u51C6\u9608\u503C**\uFF08\u9664\u975E\u7528\u6237\u660E\u786E\u6307\u5B9A\u5176\u4ED6\u503C\uFF09\uFF1A
14479
+ - \u663E\u8457\u6027\uFF1A**padj \u2264 0.05**\uFF08FDR \u6821\u6B63\uFF0CBenjamini-Hochberg\uFF09
14480
+ - \u6548\u5E94\u91CF\uFF1A**|log2FoldChange| \u2265 1**\uFF08\u5373 2 \u500D\u5DEE\u5F02\uFF09
14481
+
14482
+ ### 5. \u7ED3\u679C\u9A8C\u8BC1
14483
+ \u6BCF\u4E2A\u5206\u6790\u5B8C\u6210\u540E\uFF0C\u8F93\u51FA\u5173\u952E\u7EDF\u8BA1\u6458\u8981\uFF1A
14484
+ \`\`\`r
14485
+ message(sprintf("\u603B\u57FA\u56E0\u6570: %d | \u663E\u8457 DEG: %d (\u4E0A\u8C03: %d, \u4E0B\u8C03: %d)",
14486
+ nrow(res), nrow(sig_genes),
14487
+ sum(sig_genes$log2FoldChange > 0),
14488
+ sum(sig_genes$log2FoldChange < 0)))
14489
+ \`\`\`
14490
+
14491
+ ---
14492
+
14493
+ ## Script Execution Rules
14494
+
14495
+ \u{1F6A8} **\u5173\u952E\u89C4\u5219\uFF1A**
14496
+ 1. **\u4F18\u5148\u4F7F\u7528\u5DE5\u4F5C\u6D41\u811A\u672C**\uFF0C\u4E0D\u8981\u4ECE\u96F6\u5199\u4EE3\u7801
14497
+ 2. **\u811A\u672C\u5931\u8D25\u5904\u7406\u987A\u5E8F**: \u4FEE\u590D\u5E76\u91CD\u8BD5 \u2192 \u4FEE\u6539\u811A\u672C \u2192 \u9002\u914D\u65B9\u6848 \u2192 \u6700\u540E\u624D\u4ECE\u5934\u5199
14498
+ 3. **\u4F7F\u7528\u76F8\u5BF9\u8DEF\u5F84**\uFF1A\u5728\u5DE5\u4F5C\u6D41\u76EE\u5F55\u5185\u7528 \`source("scripts/xxx.R")\` \u800C\u975E\u7EDD\u5BF9\u8DEF\u5F84
14499
+ 4. **\u9A8C\u8BC1\u6D88\u606F**\uFF1A\u6BCF\u6B65\u5B8C\u6210\u5E94\u770B\u5230 "\u2713" \u786E\u8BA4\u6D88\u606F\uFF1B\u770B\u4E0D\u5230\u8BF4\u660E\u6CA1\u7528\u811A\u672C
14500
+
14501
+ ---
14157
14502
 
14158
14503
  ## Common Analysis Patterns
14159
14504
 
@@ -14170,7 +14515,25 @@ samtools --version 2>&1 | head -1
14170
14515
  \u4F7F\u7528 functional-enrichment-from-degs \u5DE5\u4F5C\u6D41
14171
14516
 
14172
14517
  **\u7528\u6237\u8BF4 "\u8BBE\u8BA1 CRISPR guide RNA" \u2192**
14173
- \u4F7F\u7528 molecular_biology.py \u7684 design_crispr_knockout_guides()`;
14518
+ \u4F7F\u7528 molecular_biology.py \u7684 design_crispr_knockout_guides()
14519
+
14520
+ **\u7528\u6237\u8BF4 "\u54EA\u4E9B\u57FA\u56E0\u5728\u80BF\u7624\u91CC\u8868\u8FBE\u91CF\u9AD8" / "\u627E\u4E0A\u8C03\u57FA\u56E0" \u2192**
14521
+ \u2192 \u5DEE\u5F02\u8868\u8FBE\u5206\u6790\uFF08DESeq2\uFF09\uFF0C\u4F7F\u7528 bulk-rnaseq-counts-to-de-deseq2 \u5DE5\u4F5C\u6D41
14522
+
14523
+ **\u7528\u6237\u8BF4 "\u5148\u505A\u5DEE\u5F02\u8868\u8FBE\uFF0C\u518D\u505A\u5BCC\u96C6\u5206\u6790" \u2192**
14524
+ \u2192 \u8BC6\u522B\u4E3A\u591A\u4EFB\u52A1\uFF1A\u4F9D\u6B21\u6267\u884C bulk-rnaseq-counts-to-de-deseq2 + functional-enrichment-from-degs
14525
+
14526
+ **\u7528\u6237\u8BF4 "\u8FD9\u4E9B\u57FA\u56E0\u53C2\u4E0E\u4EC0\u4E48\u901A\u8DEF" / "\u57FA\u56E0\u529F\u80FD\u662F\u4EC0\u4E48" \u2192**
14527
+ \u2192 \u529F\u80FD\u5BCC\u96C6\u5206\u6790\uFF0C\u4F7F\u7528 functional-enrichment-from-degs \u5DE5\u4F5C\u6D41
14528
+
14529
+ **\u7528\u6237\u8BF4 "\u5206\u6790\u5355\u7EC6\u80DE\u6570\u636E" / "10X\u6570\u636E\u5206\u6790" \u2192**
14530
+ \u5148\u95EE\uFF1APython \u8FD8\u662F R\uFF1F\u2192 Scanpy \u6216 Seurat
14531
+
14532
+ **\u7528\u6237\u8BF4 "\u753B\u751F\u5B58\u66F2\u7EBF" / "\u5206\u6790\u60A3\u8005\u9884\u540E" / "OS/PFS \u5206\u6790" \u2192**
14533
+ \u2192 \u751F\u5B58\u5206\u6790\uFF0C\u4F7F\u7528 survival-analysis-clinical \u5DE5\u4F5C\u6D41
14534
+
14535
+ **\u7528\u6237\u8BF4 "\u5E2E\u6211\u5206\u6790 GSE12345" / "\u4E0B\u8F7D GEO \u6570\u636E" \u2192**
14536
+ \u2192 \u7ACB\u5373\u8C03\u7528 fetch_geo("GSE12345") \u83B7\u53D6\u5143\u6570\u636E\u548C\u4E0B\u8F7D\u4EE3\u7801\uFF0C\u65E0\u9700\u8BA9\u7528\u6237\u624B\u52A8\u4E0B\u8F7D`;
14174
14537
  }
14175
14538
 
14176
14539
  // src/skillRouter.ts
@@ -14195,8 +14558,11 @@ var SKILL_ROUTES = [
14195
14558
  category: "\u8F6C\u5F55\u7EC4",
14196
14559
  tag: "workflow",
14197
14560
  keywords: [
14561
+ // exact tool names
14198
14562
  "deseq2",
14199
14563
  "edger",
14564
+ "limma-voom",
14565
+ // explicit analysis terms
14200
14566
  "rna-seq\u5DEE\u5F02",
14201
14567
  "rnaseq\u5DEE\u5F02",
14202
14568
  "\u5DEE\u5F02\u8868\u8FBE\u5206\u6790",
@@ -14204,7 +14570,21 @@ var SKILL_ROUTES = [
14204
14570
  "count\u77E9\u9635",
14205
14571
  "count matrix",
14206
14572
  "\u539F\u59CBcounts",
14207
- "raw counts"
14573
+ "raw counts",
14574
+ // natural-language synonyms (the main gap in the original)
14575
+ "\u54EA\u4E9B\u57FA\u56E0\u8868\u8FBE\u91CF\u9AD8",
14576
+ "\u54EA\u4E9B\u57FA\u56E0\u4E0A\u8C03",
14577
+ "\u54EA\u4E9B\u57FA\u56E0\u4E0B\u8C03",
14578
+ "\u57FA\u56E0\u8868\u8FBE\u5DEE\u5F02",
14579
+ "\u8F6C\u5F55\u7EC4\u5DEE\u5F02",
14580
+ "\u8868\u8FBE\u91CF\u5DEE\u5F02",
14581
+ "\u4E0A\u8C03\u57FA\u56E0",
14582
+ "\u4E0B\u8C03\u57FA\u56E0",
14583
+ "deg\u5206\u6790",
14584
+ "differentially expressed",
14585
+ "\u5DEE\u5F02\u5206\u6790",
14586
+ "\u6BD4\u8F83\u4E24\u7EC4\u57FA\u56E0\u8868\u8FBE",
14587
+ "\u80BF\u7624vs\u6B63\u5E38"
14208
14588
  ]
14209
14589
  },
14210
14590
  {
@@ -14220,7 +14600,16 @@ var SKILL_ROUTES = [
14220
14600
  "hdbscan",
14221
14601
  "\u6837\u672C\u805A\u7C7B",
14222
14602
  "\u7279\u5F81\u805A\u7C7B",
14223
- "omics clustering"
14603
+ "omics clustering",
14604
+ // natural-language synonyms
14605
+ "\u6837\u672C\u5206\u7EC4",
14606
+ "\u6837\u672C\u5206\u7C7B",
14607
+ "\u805A\u7C7B\u5206\u6790",
14608
+ "\u65E0\u76D1\u7763\u805A\u7C7B",
14609
+ "\u70ED\u56FE\u805A\u7C7B",
14610
+ "pca\u5206\u6790",
14611
+ "\u4E3B\u6210\u5206\u5206\u6790",
14612
+ "umap\u964D\u7EF4"
14224
14613
  ]
14225
14614
  },
14226
14615
  {
@@ -14236,7 +14625,16 @@ var SKILL_ROUTES = [
14236
14625
  "10x chromium",
14237
14626
  "leiden\u805A\u7C7B",
14238
14627
  "python\u5355\u7EC6\u80DE",
14239
- "anndata\u5206\u6790"
14628
+ "anndata\u5206\u6790",
14629
+ // natural-language synonyms
14630
+ "\u5355\u7EC6\u80DE\u6D4B\u5E8F",
14631
+ "\u5355\u7EC6\u80DE\u5206\u6790",
14632
+ "\u5355\u7EC6\u80DE\u6570\u636E",
14633
+ "10x\u6570\u636E",
14634
+ "\u7EC6\u80DE\u805A\u7C7B",
14635
+ "\u7EC6\u80DE\u7C7B\u578B\u9274\u5B9A",
14636
+ "scrna",
14637
+ "sc-rna"
14240
14638
  ]
14241
14639
  },
14242
14640
  {
@@ -14250,7 +14648,11 @@ var SKILL_ROUTES = [
14250
14648
  "findclusters",
14251
14649
  "findneighbors",
14252
14650
  "sctransform",
14253
- "r\u5355\u7EC6\u80DE\u5206\u6790"
14651
+ "r\u5355\u7EC6\u80DE\u5206\u6790",
14652
+ // natural-language synonyms
14653
+ "r\u505A\u5355\u7EC6\u80DE",
14654
+ "seurat\u5206\u6790",
14655
+ "\u7528r\u5206\u6790\u5355\u7EC6\u80DE"
14254
14656
  ]
14255
14657
  },
14256
14658
  {
@@ -14266,7 +14668,13 @@ var SKILL_ROUTES = [
14266
14668
  "spatial deconvolution",
14267
14669
  "\u914D\u4F53\u53D7\u4F53\u5206\u6790",
14268
14670
  "\u7A7A\u95F4\u57FA\u56E0\u8868\u8FBE",
14269
- "stereo-seq"
14671
+ "stereo-seq",
14672
+ // natural-language synonyms
14673
+ "\u7A7A\u95F4\u7EC4\u5B66",
14674
+ "\u7EC4\u7EC7\u5207\u7247\u6D4B\u5E8F",
14675
+ "\u7A7A\u95F4\u5206\u8FA8\u7387\u8F6C\u5F55\u7EC4",
14676
+ "\u7EC6\u80DE\u7A7A\u95F4\u5206\u5E03",
14677
+ "\u7A7A\u95F4\u5355\u7EC6\u80DE"
14270
14678
  ]
14271
14679
  },
14272
14680
  {
@@ -14280,7 +14688,12 @@ var SKILL_ROUTES = [
14280
14688
  "coexpression network",
14281
14689
  "\u57FA\u56E0\u5171\u8868\u8FBE\u6A21\u5757",
14282
14690
  "weighted gene coexpression",
14283
- "\u4E0E\u8868\u578B\u76F8\u5173\u7684\u57FA\u56E0\u6A21\u5757"
14691
+ "\u4E0E\u8868\u578B\u76F8\u5173\u7684\u57FA\u56E0\u6A21\u5757",
14692
+ // natural-language synonyms
14693
+ "\u57FA\u56E0\u6A21\u5757",
14694
+ "\u5171\u8868\u8FBE",
14695
+ "\u57FA\u56E0\u7F51\u7EDC",
14696
+ "\u57FA\u56E0\u76F8\u5173\u6027\u7F51\u7EDC"
14284
14697
  ]
14285
14698
  },
14286
14699
  {
@@ -14300,7 +14713,19 @@ var SKILL_ROUTES = [
14300
14713
  "functional enrichment",
14301
14714
  "\u529F\u80FD\u5BCC\u96C6",
14302
14715
  "deg\u5BCC\u96C6",
14303
- "\u5DEE\u5F02\u57FA\u56E0\u901A\u8DEF"
14716
+ "\u5DEE\u5F02\u57FA\u56E0\u901A\u8DEF",
14717
+ // natural-language synonyms
14718
+ "\u8FD9\u4E9B\u57FA\u56E0\u53C2\u4E0E\u4EC0\u4E48\u901A\u8DEF",
14719
+ "\u57FA\u56E0\u529F\u80FD\u6CE8\u91CA",
14720
+ "\u4FE1\u53F7\u901A\u8DEF",
14721
+ "\u751F\u7269\u5B66\u8FC7\u7A0B",
14722
+ "\u5206\u5B50\u529F\u80FD",
14723
+ "\u7EC6\u80DE\u7EC4\u5206",
14724
+ "go\u5BCC\u96C6",
14725
+ "\u901A\u8DEF\u5BCC\u96C6",
14726
+ "\u57FA\u56E0\u96C6\u5206\u6790",
14727
+ "ora\u5206\u6790",
14728
+ "gsea\u5206\u6790"
14304
14729
  ]
14305
14730
  },
14306
14731
  {
@@ -14315,7 +14740,12 @@ var SKILL_ROUTES = [
14315
14740
  "gene regulatory network",
14316
14741
  "\u8F6C\u5F55\u56E0\u5B50\u8C03\u63A7\u5B50",
14317
14742
  "tf regulon",
14318
- "grn\u63A8\u65AD"
14743
+ "grn\u63A8\u65AD",
14744
+ // natural-language synonyms
14745
+ "\u8F6C\u5F55\u56E0\u5B50\u5206\u6790",
14746
+ "\u8F6C\u5F55\u56E0\u5B50\u9776\u57FA\u56E0",
14747
+ "tf\u6D3B\u6027",
14748
+ "\u8C03\u63A7\u7F51\u7EDC\u63A8\u65AD"
14319
14749
  ]
14320
14750
  },
14321
14751
  // ── Genomics ──────────────────────────────────────────────────────────────────
@@ -14334,7 +14764,14 @@ var SKILL_ROUTES = [
14334
14764
  "annovar",
14335
14765
  "\u53D8\u5F02\u81F4\u75C5\u6027\u9884\u6D4B",
14336
14766
  "\u53D8\u5F02\u529F\u80FD\u9884\u6D4B",
14337
- "clinvar\u6CE8\u91CA"
14767
+ "clinvar\u6CE8\u91CA",
14768
+ // natural-language synonyms
14769
+ "\u7A81\u53D8\u6CE8\u91CA",
14770
+ "\u53D8\u5F02\u89E3\u8BFB",
14771
+ "vcf\u6587\u4EF6\u5206\u6790",
14772
+ "\u9057\u4F20\u53D8\u5F02\u89E3\u6790",
14773
+ "\u81F4\u75C5\u6027\u8BC4\u4F30",
14774
+ "\u53D8\u5F02\u5F71\u54CD\u9884\u6D4B"
14338
14775
  ]
14339
14776
  },
14340
14777
  {
@@ -14350,7 +14787,14 @@ var SKILL_ROUTES = [
14350
14787
  "\u5168\u57FA\u56E0\u7EC4\u5173\u8054\u5206\u6790",
14351
14788
  "genome-wide association",
14352
14789
  "\u56E0\u679C\u57FA\u56E0\u9274\u5B9A",
14353
- "qtl\u6574\u5408"
14790
+ "qtl\u6574\u5408",
14791
+ // natural-language synonyms
14792
+ "gwas\u7ED3\u679C\u89E3\u8BFB",
14793
+ "gwas\u529F\u80FD\u6CE8\u91CA",
14794
+ "\u5173\u8054\u4F4D\u70B9\u57FA\u56E0",
14795
+ "snp\u529F\u80FD",
14796
+ "gwas\u4FE1\u53F7",
14797
+ "\u9057\u4F20\u5173\u8054\u5206\u6790"
14354
14798
  ]
14355
14799
  },
14356
14800
  {
@@ -14366,7 +14810,12 @@ var SKILL_ROUTES = [
14366
14810
  "ivw\u65B9\u6CD5",
14367
14811
  "mr-egger",
14368
14812
  "\u53CC\u6837\u672Cmr",
14369
- "\u5DE5\u5177\u53D8\u91CFiv"
14813
+ "\u5DE5\u5177\u53D8\u91CFiv",
14814
+ // natural-language synonyms
14815
+ "\u56E0\u679C\u63A8\u65AD",
14816
+ "\u66B4\u9732\u4E0E\u7ED3\u5C40",
14817
+ "\u9057\u4F20\u5DE5\u5177\u53D8\u91CF",
14818
+ "mr\u5206\u6790"
14370
14819
  ]
14371
14820
  },
14372
14821
  {
@@ -14380,7 +14829,11 @@ var SKILL_ROUTES = [
14380
14829
  "\u591A\u57FA\u56E0\u98CE\u9669\u8BC4\u5206",
14381
14830
  "prs-cs",
14382
14831
  "\u9057\u4F20\u98CE\u9669\u9884\u6D4B",
14383
- "prs\u8BA1\u7B97"
14832
+ "prs\u8BA1\u7B97",
14833
+ // natural-language synonyms
14834
+ "\u9057\u4F20\u98CE\u9669\u8BC4\u5206",
14835
+ "\u591A\u57FA\u56E0\u8BC4\u5206",
14836
+ "\u75BE\u75C5\u9057\u4F20\u98CE\u9669"
14384
14837
  ]
14385
14838
  },
14386
14839
  {
@@ -14395,7 +14848,13 @@ var SKILL_ROUTES = [
14395
14848
  "bagel2",
14396
14849
  "sgrna\u7B5B\u9009",
14397
14850
  "pooled crispr",
14398
- "crispr hit\u8BC6\u522B"
14851
+ "crispr hit\u8BC6\u522B",
14852
+ // natural-language synonyms
14853
+ "crispr\u7B5B\u9009",
14854
+ "\u529F\u80FD\u57FA\u56E0\u7EC4\u7B5B\u9009",
14855
+ "\u5FC5\u9700\u57FA\u56E0\u7B5B\u9009",
14856
+ "crispr\u6572\u9664\u7B5B\u9009",
14857
+ "crispr\u6587\u5E93"
14399
14858
  ]
14400
14859
  },
14401
14860
  // ── Epigenomics ───────────────────────────────────────────────────────────────
@@ -14409,7 +14868,13 @@ var SKILL_ROUTES = [
14409
14868
  "chip-seq\u5CF0\u503C\u5BCC\u96C6",
14410
14869
  "peak enrichment chip",
14411
14870
  "chip atlas\u6570\u636E\u5E93",
14412
- "histone chip\u5206\u6790"
14871
+ "histone chip\u5206\u6790",
14872
+ // natural-language synonyms
14873
+ "chip-seq\u5206\u6790",
14874
+ "chip\u6570\u636E",
14875
+ "\u7EC4\u86CB\u767D\u4FEE\u9970",
14876
+ "h3k27ac",
14877
+ "h3k4me3"
14413
14878
  ]
14414
14879
  },
14415
14880
  {
@@ -14422,7 +14887,11 @@ var SKILL_ROUTES = [
14422
14887
  "differential binding",
14423
14888
  "\u5DEE\u5F02chip-seq",
14424
14889
  "differential peak",
14425
- "chip-seq\u6761\u4EF6\u6BD4\u8F83"
14890
+ "chip-seq\u6761\u4EF6\u6BD4\u8F83",
14891
+ // natural-language synonyms
14892
+ "\u5DEE\u5F02\u5CF0",
14893
+ "\u5DEE\u5F02\u7ED3\u5408\u4F4D\u70B9",
14894
+ "chip-seq\u5DEE\u5F02"
14426
14895
  ]
14427
14896
  },
14428
14897
  {
@@ -14436,7 +14905,12 @@ var SKILL_ROUTES = [
14436
14905
  "\u8F6C\u5F55\u56E0\u5B50\u9776\u57FA\u56E0chip",
14437
14906
  "tf\u9776\u57FA\u56E0",
14438
14907
  "peak annotation\u9776\u57FA\u56E0",
14439
- "chip-seq peak\u6CE8\u91CA"
14908
+ "chip-seq peak\u6CE8\u91CA",
14909
+ // natural-language synonyms
14910
+ "peak\u6CE8\u91CA",
14911
+ "\u5CF0\u503C\u6CE8\u91CA",
14912
+ "\u8F6C\u5F55\u56E0\u5B50\u7ED3\u5408\u4F4D\u70B9",
14913
+ "chip\u9776\u70B9"
14440
14914
  ]
14441
14915
  },
14442
14916
  // ── Clinical ──────────────────────────────────────────────────────────────────
@@ -14450,7 +14924,12 @@ var SKILL_ROUTES = [
14450
14924
  "clinical trial landscape",
14451
14925
  "ct.gov\u6570\u636E\u5206\u6790",
14452
14926
  "\u4E34\u5E8A\u8BD5\u9A8C\u683C\u5C40",
14453
- "\u4E34\u5E8A\u7814\u7A76\u5206\u6790"
14927
+ "\u4E34\u5E8A\u7814\u7A76\u5206\u6790",
14928
+ // natural-language synonyms
14929
+ "\u4E34\u5E8A\u8BD5\u9A8C",
14930
+ "\u5728\u7814\u836F\u7269",
14931
+ "\u4E34\u5E8A\u7BA1\u7EBF",
14932
+ "\u4E34\u5E8A\u7814\u7A76\u73B0\u72B6"
14454
14933
  ]
14455
14934
  },
14456
14935
  {
@@ -14463,7 +14942,12 @@ var SKILL_ROUTES = [
14463
14942
  "preclinical literature",
14464
14943
  "\u7CFB\u7EDF\u6587\u732E\u63D0\u53D6",
14465
14944
  "literature extraction",
14466
- "\u6587\u732E\u7CFB\u7EDF\u7EFC\u5408"
14945
+ "\u6587\u732E\u7CFB\u7EDF\u7EFC\u5408",
14946
+ // natural-language synonyms
14947
+ "\u6587\u732E\u7EFC\u8FF0",
14948
+ "\u7CFB\u7EDF\u7EFC\u8FF0",
14949
+ "\u6587\u732E\u6574\u7406",
14950
+ "\u6587\u732E\u6316\u6398"
14467
14951
  ]
14468
14952
  },
14469
14953
  {
@@ -14480,7 +14964,16 @@ var SKILL_ROUTES = [
14480
14964
  "\u5B9E\u9A8C\u8BBE\u8BA1\u7EDF\u8BA1",
14481
14965
  "\u5047\u8BBE\u68C0\u9A8C\u9009\u62E9",
14482
14966
  "t\u68C0\u9A8C\u8FD8\u662F",
14483
- "anova\u65B9\u5DEE\u5206\u6790"
14967
+ "anova\u65B9\u5DEE\u5206\u6790",
14968
+ // natural-language synonyms
14969
+ "\u7528\u4EC0\u4E48\u7EDF\u8BA1\u65B9\u6CD5",
14970
+ "\u7EDF\u8BA1\u663E\u8457\u6027",
14971
+ "\u68C0\u9A8C\u65B9\u6CD5",
14972
+ "\u9700\u8981\u591A\u5C11\u6837\u672C",
14973
+ "\u529F\u6548\u5206\u6790",
14974
+ "\u7EDF\u8BA1\u529F\u6548",
14975
+ "p\u503C",
14976
+ "\u663E\u8457\u6027\u68C0\u9A8C"
14484
14977
  ]
14485
14978
  },
14486
14979
  {
@@ -14494,7 +14987,14 @@ var SKILL_ROUTES = [
14494
14987
  "biomarker panel\u7B5B\u9009",
14495
14988
  "\u6700\u5C0F\u6807\u5FD7\u7269\u9762\u677F",
14496
14989
  "feature selection lasso",
14497
- "\u8BCA\u65AD\u6807\u5FD7\u7269\u7B5B\u9009"
14990
+ "\u8BCA\u65AD\u6807\u5FD7\u7269\u7B5B\u9009",
14991
+ // natural-language synonyms
14992
+ "\u751F\u7269\u6807\u5FD7\u7269\u7B5B\u9009",
14993
+ "\u6807\u5FD7\u7269\u7EC4\u5408",
14994
+ "\u8BCA\u65AD\u6A21\u578B",
14995
+ "\u9884\u6D4B\u6A21\u578B\u7279\u5F81\u7B5B\u9009",
14996
+ "biomarker",
14997
+ "\u7279\u5F81\u9009\u62E9"
14498
14998
  ]
14499
14999
  },
14500
15000
  {
@@ -14510,7 +15010,64 @@ var SKILL_ROUTES = [
14510
15010
  "primer3",
14511
15011
  "qrt-pcr\u8BBE\u8BA1",
14512
15012
  "\u6269\u589E\u5B50\u8BBE\u8BA1",
14513
- "\u5F15\u7269\u7279\u5F02\u6027\u9A8C\u8BC1"
15013
+ "\u5F15\u7269\u7279\u5F02\u6027\u9A8C\u8BC1",
15014
+ // natural-language synonyms
15015
+ "\u8BBE\u8BA1\u5F15\u7269",
15016
+ "\u6269\u589E\u5F15\u7269",
15017
+ "rt-pcr\u5F15\u7269",
15018
+ "\u5B9A\u91CFpcr"
15019
+ ]
15020
+ },
15021
+ // ── Survival Analysis ─────────────────────────────────────────────────────────
15022
+ {
15023
+ id: "survival-analysis-clinical",
15024
+ name: "\u4E34\u5E8A\u751F\u5B58\u5206\u6790 (KM + Cox)",
15025
+ category: "\u4E34\u5E8A",
15026
+ tag: "workflow",
15027
+ keywords: [
15028
+ // exact method names
15029
+ "kaplan-meier",
15030
+ "kaplan meier",
15031
+ "km\u66F2\u7EBF",
15032
+ "cox\u56DE\u5F52",
15033
+ "cox regression",
15034
+ "\u751F\u5B58\u5206\u6790",
15035
+ "survival analysis",
15036
+ "\u751F\u5B58\u66F2\u7EBF",
15037
+ "log-rank",
15038
+ // outcome types
15039
+ "\u603B\u751F\u5B58\u671F",
15040
+ "overall survival",
15041
+ "os\u5206\u6790",
15042
+ "\u65E0\u8FDB\u5C55\u751F\u5B58",
15043
+ "progression-free survival",
15044
+ "pfs\u5206\u6790",
15045
+ "\u65E0\u75C5\u751F\u5B58",
15046
+ "disease-free survival",
15047
+ "dfs\u5206\u6790",
15048
+ "\u590D\u53D1\u751F\u5B58",
15049
+ "relapse-free survival",
15050
+ "rfs\u5206\u6790",
15051
+ // natural-language synonyms
15052
+ "\u60A3\u8005\u9884\u540E",
15053
+ "\u9884\u540E\u5206\u6790",
15054
+ "\u751F\u5B58\u9884\u540E",
15055
+ "\u4E34\u5E8A\u9884\u540E",
15056
+ "\u5220\u5931\u6570\u636E",
15057
+ "\u53F3\u5220\u5931",
15058
+ "censored data",
15059
+ "\u98CE\u9669\u6BD4",
15060
+ "hazard ratio",
15061
+ "hr\u503C",
15062
+ "\u7ADE\u4E89\u98CE\u9669",
15063
+ "competing risk",
15064
+ "fine-gray",
15065
+ "\u4E2D\u4F4D\u751F\u5B58\u65F6\u95F4",
15066
+ "5\u5E74\u751F\u5B58\u7387",
15067
+ "3\u5E74\u751F\u5B58\u7387",
15068
+ "\u9AD8\u8868\u8FBE\u9884\u540E\u5DEE",
15069
+ "\u57FA\u56E0\u8868\u8FBE\u4E0E\u9884\u540E",
15070
+ "\u7A81\u53D8\u4E0E\u9884\u540E"
14514
15071
  ]
14515
15072
  },
14516
15073
  // ── OpenClaw Key Skills ────────────────────────────────────────────────────────
@@ -14713,36 +15270,166 @@ function routeSkill(message) {
14713
15270
  let score = 0;
14714
15271
  for (const kw of route.keywords) {
14715
15272
  if (lower.includes(kw.toLowerCase())) {
14716
- score += kw.length;
15273
+ score += 1 + kw.length * 0.1;
14717
15274
  }
14718
15275
  }
14719
15276
  if (score > 0) scores.set(route.id, { route, score });
14720
15277
  }
14721
- const sorted = Array.from(scores.values()).sort((a2, b2) => b2.score - a2.score).slice(0, 3);
15278
+ const sorted = Array.from(scores.values()).sort((a2, b2) => b2.score - a2.score).slice(0, 5);
14722
15279
  return {
14723
15280
  routes: sorted.map((v2) => v2.route),
14724
15281
  topScore: sorted[0]?.score ?? 0
14725
15282
  };
14726
15283
  }
14727
15284
 
15285
+ // src/sessions.ts
15286
+ var import_fs4 = require("fs");
15287
+ var import_path4 = require("path");
15288
+ var SESSIONS_DIR = (0, import_path4.join)(BGI_DIR, "sessions");
15289
+ var CHECKPOINTS_DIR = (0, import_path4.join)(BGI_DIR, "checkpoints");
15290
+ function ensureSessionDirs() {
15291
+ for (const d2 of [SESSIONS_DIR, CHECKPOINTS_DIR]) {
15292
+ if (!(0, import_fs4.existsSync)(d2)) (0, import_fs4.mkdirSync)(d2, { recursive: true });
15293
+ }
15294
+ }
15295
+ function sessionPath(id) {
15296
+ return (0, import_path4.join)(SESSIONS_DIR, `${id}.json`);
15297
+ }
15298
+ function newSessionId() {
15299
+ const now = /* @__PURE__ */ new Date();
15300
+ const date = now.toISOString().slice(0, 10).replace(/-/g, "");
15301
+ const rand = Math.random().toString(36).slice(2, 6);
15302
+ return `${date}-${rand}`;
15303
+ }
15304
+ function saveSession(id, name, messages, skills, createdAt) {
15305
+ ensureSessionDirs();
15306
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15307
+ const firstUser = messages.find((m2) => m2.role === "user");
15308
+ const rawPreview = typeof firstUser?.content === "string" ? firstUser.content : "";
15309
+ const preview = rawPreview.replace(/\[Skill 已加载[^\]]*\][\s\S]*/, "").trim().slice(0, 80);
15310
+ const session = {
15311
+ id,
15312
+ name,
15313
+ createdAt,
15314
+ updatedAt: now,
15315
+ messageCount: messages.length,
15316
+ skills,
15317
+ preview,
15318
+ messages
15319
+ };
15320
+ (0, import_fs4.writeFileSync)(sessionPath(id), JSON.stringify(session, null, 2), "utf8");
15321
+ }
15322
+ function loadSession(id) {
15323
+ ensureSessionDirs();
15324
+ const p2 = sessionPath(id);
15325
+ if (!(0, import_fs4.existsSync)(p2)) return null;
15326
+ try {
15327
+ return JSON.parse((0, import_fs4.readFileSync)(p2, "utf8"));
15328
+ } catch {
15329
+ return null;
15330
+ }
15331
+ }
15332
+ function listSessions() {
15333
+ ensureSessionDirs();
15334
+ const files = (0, import_fs4.readdirSync)(SESSIONS_DIR).filter((f2) => f2.endsWith(".json"));
15335
+ const metas = [];
15336
+ for (const f2 of files) {
15337
+ try {
15338
+ const raw = JSON.parse((0, import_fs4.readFileSync)((0, import_path4.join)(SESSIONS_DIR, f2), "utf8"));
15339
+ metas.push({
15340
+ id: raw.id,
15341
+ name: raw.name,
15342
+ createdAt: raw.createdAt,
15343
+ updatedAt: raw.updatedAt,
15344
+ messageCount: raw.messageCount,
15345
+ skills: raw.skills ?? [],
15346
+ preview: raw.preview ?? ""
15347
+ });
15348
+ } catch {
15349
+ }
15350
+ }
15351
+ return metas.sort((a2, b2) => b2.updatedAt.localeCompare(a2.updatedAt));
15352
+ }
15353
+ function deleteSession(id) {
15354
+ const p2 = sessionPath(id);
15355
+ if (!(0, import_fs4.existsSync)(p2)) return false;
15356
+ (0, import_fs4.unlinkSync)(p2);
15357
+ return true;
15358
+ }
15359
+ function getLastSession() {
15360
+ const all = listSessions();
15361
+ return all[0] ?? null;
15362
+ }
15363
+ function checkpointPath(id) {
15364
+ return (0, import_path4.join)(CHECKPOINTS_DIR, `${id}.json`);
15365
+ }
15366
+ function saveCheckpoint(sessionId, label, messages, skills) {
15367
+ ensureSessionDirs();
15368
+ const id = `${sessionId}-cp${Date.now()}`;
15369
+ const cp = {
15370
+ id,
15371
+ sessionId,
15372
+ label,
15373
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
15374
+ messageCount: messages.length,
15375
+ messages,
15376
+ skills
15377
+ };
15378
+ (0, import_fs4.writeFileSync)(checkpointPath(id), JSON.stringify(cp, null, 2), "utf8");
15379
+ return id;
15380
+ }
15381
+ function listCheckpoints(sessionId) {
15382
+ ensureSessionDirs();
15383
+ const files = (0, import_fs4.readdirSync)(CHECKPOINTS_DIR).filter((f2) => f2.endsWith(".json"));
15384
+ const cps = [];
15385
+ for (const f2 of files) {
15386
+ try {
15387
+ const cp = JSON.parse((0, import_fs4.readFileSync)((0, import_path4.join)(CHECKPOINTS_DIR, f2), "utf8"));
15388
+ if (!sessionId || cp.sessionId === sessionId) cps.push(cp);
15389
+ } catch {
15390
+ }
15391
+ }
15392
+ return cps.sort((a2, b2) => b2.createdAt.localeCompare(a2.createdAt));
15393
+ }
15394
+ function deleteCheckpoint(id) {
15395
+ const p2 = checkpointPath(id);
15396
+ if (!(0, import_fs4.existsSync)(p2)) return false;
15397
+ (0, import_fs4.unlinkSync)(p2);
15398
+ return true;
15399
+ }
15400
+ function clearCheckpoints(sessionId) {
15401
+ const cps = listCheckpoints(sessionId);
15402
+ let count = 0;
15403
+ for (const cp of cps) {
15404
+ if (deleteCheckpoint(cp.id)) count++;
15405
+ }
15406
+ return count;
15407
+ }
15408
+
14728
15409
  // src/index.ts
14729
- var VERSION2 = "2.2.6";
15410
+ var import_fs6 = require("fs");
15411
+ var VERSION2 = "2.2.8";
15412
+ var SESSION_CTX = {
15413
+ id: "",
15414
+ createdAt: "",
15415
+ wdirSnapshot: null
15416
+ };
14730
15417
  function installBundledData() {
14731
- const bundledData = (0, import_path4.join)(__dirname, "..", "data");
14732
- if (!(0, import_fs4.existsSync)(bundledData)) return;
15418
+ const bundledData = (0, import_path5.join)(__dirname, "..", "data");
15419
+ if (!(0, import_fs5.existsSync)(bundledData)) return;
14733
15420
  ensureDirs();
14734
15421
  const targets = [
14735
- { src: (0, import_path4.join)(bundledData, "workflows"), dest: WORKFLOWS_DIR, name: "Skills (\u751F\u4FE1\u5DE5\u4F5C\u6D41)" },
14736
- { src: (0, import_path4.join)(bundledData, "skills"), dest: SKILLS_DIR, name: "Skills (\u533B\u5B66\u4E13\u79D1)" },
14737
- { src: (0, import_path4.join)(bundledData, "tools"), dest: TOOLS_DIR, name: "\u5DE5\u5177" }
15422
+ { src: (0, import_path5.join)(bundledData, "workflows"), dest: WORKFLOWS_DIR, name: "Skills (\u751F\u4FE1\u5DE5\u4F5C\u6D41)" },
15423
+ { src: (0, import_path5.join)(bundledData, "skills"), dest: SKILLS_DIR, name: "Skills (\u533B\u5B66\u4E13\u79D1)" },
15424
+ { src: (0, import_path5.join)(bundledData, "tools"), dest: TOOLS_DIR, name: "\u5DE5\u5177" }
14738
15425
  ];
14739
15426
  let installed = false;
14740
15427
  for (const { src, dest, name } of targets) {
14741
- if (!(0, import_fs4.existsSync)(src)) continue;
14742
- const isEmpty = !(0, import_fs4.existsSync)(dest) || (0, import_fs4.readdirSync)(dest).length === 0;
15428
+ if (!(0, import_fs5.existsSync)(src)) continue;
15429
+ const isEmpty = !(0, import_fs5.existsSync)(dest) || (0, import_fs5.readdirSync)(dest).length === 0;
14743
15430
  if (isEmpty) {
14744
- (0, import_fs4.mkdirSync)(dest, { recursive: true });
14745
- (0, import_fs4.cpSync)(src, dest, { recursive: true });
15431
+ (0, import_fs5.mkdirSync)(dest, { recursive: true });
15432
+ (0, import_fs5.cpSync)(src, dest, { recursive: true });
14746
15433
  if (!installed) {
14747
15434
  process.stdout.write(source_default.dim("\u6B63\u5728\u521D\u59CB\u5316\u5185\u7F6E\u6570\u636E...\n"));
14748
15435
  installed = true;
@@ -14777,22 +15464,49 @@ function printHelp() {
14777
15464
  console.log(` ${source_default.cyan("/clear")} \u6E05\u7A7A\u5BF9\u8BDD\u5386\u53F2`);
14778
15465
  console.log(` ${source_default.cyan("/history")} \u67E5\u770B\u5BF9\u8BDD\u7EDF\u8BA1\uFF08\u8F6E\u6B21 / Token \u4F30\u7B97\uFF09`);
14779
15466
  console.log(` ${source_default.cyan("/compact")} \u7ACB\u5373\u538B\u7F29\u5BF9\u8BDD\u5386\u53F2\uFF08\u8D85 60k token \u81EA\u52A8\u89E6\u53D1\uFF09`);
14780
- console.log(` ${source_default.cyan("/save")} [\u6587\u4EF6\u540D] \u4FDD\u5B58\u5BF9\u8BDD\u4E3A Markdown \u6587\u4EF6`);
15467
+ console.log(` ${source_default.cyan("/save")} [\u540D\u79F0] \u4FDD\u5B58\u5BF9\u8BDD\u4E3A Markdown \u6587\u4EF6`);
14781
15468
  console.log(` ${source_default.cyan("/think")} [on|off] \u5207\u6362\u601D\u8003\u6A21\u5F0F (Qwen3 /think \u524D\u7F00)`);
14782
15469
  console.log();
15470
+ console.log(source_default.bold.cyan("\u2500\u2500\u2500 \u4F1A\u8BDD\u6301\u4E45\u5316 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
15471
+ console.log(` ${source_default.cyan("/sessions")} \u5217\u51FA\u5386\u53F2\u4F1A\u8BDD`);
15472
+ console.log(` ${source_default.cyan("/resume")} [id] \u6062\u590D\u4E0A\u6B21\uFF08\u6216\u6307\u5B9A\uFF09\u4F1A\u8BDD`);
15473
+ console.log(` ${source_default.cyan("/session-save")} [\u540D\u79F0] \u624B\u52A8\u547D\u540D\u4FDD\u5B58\u5F53\u524D\u4F1A\u8BDD`);
15474
+ console.log(` ${source_default.cyan("/session-del")} <id> \u5220\u9664\u6307\u5B9A\u4F1A\u8BDD`);
15475
+ console.log();
15476
+ console.log(source_default.bold.cyan("\u2500\u2500\u2500 \u65AD\u70B9\u7EED\u4F20 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
15477
+ console.log(` ${source_default.cyan("/checkpoint")} \u4FDD\u5B58\u5F53\u524D\u5BF9\u8BDD\u65AD\u70B9`);
15478
+ console.log(` ${source_default.cyan("/checkpoint list")} \u5217\u51FA\u5F53\u524D\u4F1A\u8BDD\u6240\u6709\u65AD\u70B9`);
15479
+ console.log(` ${source_default.cyan("/checkpoint restore")} <id> \u6062\u590D\u5230\u6307\u5B9A\u65AD\u70B9`);
15480
+ console.log(` ${source_default.cyan("/checkpoint clear")} \u6E05\u9664\u5F53\u524D\u4F1A\u8BDD\u6240\u6709\u65AD\u70B9`);
15481
+ console.log();
14783
15482
  console.log(source_default.bold.cyan("\u2500\u2500\u2500 Skills \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
14784
15483
  console.log(` ${source_default.cyan("/cat")} \u6309\u9886\u57DF\u6D4F\u89C8 Skills \u5206\u7C7B\u76EE\u5F55`);
14785
15484
  console.log(` ${source_default.cyan("/sk")} \u5217\u51FA\u5168\u90E8 Skills`);
14786
15485
  console.log(` ${source_default.cyan("/sk")} <\u5173\u952E\u8BCD> \u6A21\u7CCA\u641C\u7D22\uFF0C\u5339\u914D\u5219\u6CE8\u5165\uFF0C\u5426\u5219\u5217\u51FA\u5019\u9009`);
14787
15486
  console.log(` ${source_default.cyan("/wf")} \u540C /sk\uFF0C\u522B\u540D`);
14788
- console.log(source_default.dim(" \u793A\u4F8B: /cat /sk deseq2 /sk pubmed /sk alphafold /sk crispr"));
14789
- console.log(source_default.dim(" \u63D0\u793A: \u76F4\u63A5\u63CF\u8FF0\u4EFB\u52A1\uFF0CAI \u4F1A\u81EA\u52A8\u8BC6\u522B\u5E76\u6FC0\u6D3B\u5BF9\u5E94\u6280\u80FD"));
15487
+ console.log(` ${source_default.cyan("/skills")} \u67E5\u770B\u5F53\u524D\u4F1A\u8BDD\u5DF2\u52A0\u8F7D\u7684 Skills`);
15488
+ console.log(` ${source_default.cyan("/unload")} <id> \u4ECE\u5F53\u524D\u4F1A\u8BDD\u5378\u8F7D\u6307\u5B9A Skill`);
15489
+ console.log(source_default.dim(" \u793A\u4F8B: /cat /sk deseq2 /skills /unload deseq2"));
15490
+ console.log(source_default.dim(" \u63D0\u793A: \u76F4\u63A5\u63CF\u8FF0\u4EFB\u52A1\uFF0CAI \u4F1A\u81EA\u52A8\u8BC6\u522B\u5E76\u6FC0\u6D3B\u5BF9\u5E94\u6280\u80FD\uFF08\u52A0\u8F7D\u524D\u4F1A\u8BE2\u95EE\u786E\u8BA4\uFF09"));
15491
+ console.log();
15492
+ console.log(source_default.bold.cyan("\u2500\u2500\u2500 \u76F4\u63A5\u6267\u884C \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
15493
+ console.log(` ${source_default.cyan("!<\u547D\u4EE4>")} \u7ED5\u8FC7 AI \u76F4\u63A5\u6267\u884C Shell \u547D\u4EE4\uFF08\u5B9E\u65F6\u8F93\u51FA\uFF09`);
15494
+ console.log(source_default.dim(" \u793A\u4F8B: !ls -la !Rscript analysis.R !python script.py !samtools view -h a.bam"));
15495
+ console.log();
15496
+ console.log(source_default.bold.cyan("\u2500\u2500\u2500 \u5DE5\u4F5C\u6D41\u5411\u5BFC \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
15497
+ console.log(` ${source_default.cyan("/run")} <skill-id> \u4EA4\u4E92\u5F0F\u53C2\u6570\u5411\u5BFC\uFF0C\u81EA\u52A8\u751F\u6210\u5E76\u6267\u884C\u5206\u6790\u811A\u672C`);
15498
+ console.log(` ${source_default.cyan("/check-env")} [id] \u68C0\u6D4B Skill \u6240\u9700 R/Python \u5305\u662F\u5426\u5DF2\u5B89\u88C5`);
15499
+ console.log(` ${source_default.cyan("/install")} <url> \u4ECE GitHub \u5B89\u88C5\u7B2C\u4E09\u65B9 Skill`);
15500
+ console.log(` ${source_default.cyan("/uninstall")} <id> \u5378\u8F7D\u5DF2\u5B89\u88C5\u7684\u7B2C\u4E09\u65B9 Skill`);
14790
15501
  console.log();
14791
15502
  console.log(source_default.bold.cyan("\u2500\u2500\u2500 \u6587\u4EF6 & \u76EE\u5F55 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
14792
15503
  console.log(` ${source_default.cyan("/cd")} <\u8DEF\u5F84> \u66F4\u6539\u5DE5\u4F5C\u76EE\u5F55`);
14793
15504
  console.log(` ${source_default.cyan("/cwd")} \u663E\u793A\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55`);
15505
+ console.log(` ${source_default.cyan("/diff")} \u663E\u793A\u672C\u6B21\u4F1A\u8BDD\u65B0\u589E/\u4FEE\u6539\u7684\u6587\u4EF6`);
14794
15506
  console.log(` ${source_default.cyan("/tools")} \u5217\u51FA AI \u53EF\u8C03\u7528\u7684\u5DE5\u5177`);
14795
15507
  console.log(` ${source_default.cyan("@\u8DEF\u5F84")} \u6D88\u606F\u4E2D\u5185\u5D4C\u6587\u4EF6\u5185\u5BB9 (\u4F8B: @data.csv \u91CC\u6709\u4EC0\u4E48?)`);
15508
+ console.log(` ${source_default.cyan("@\u76EE\u5F55/")} \u5185\u5D4C\u76EE\u5F55\u4E0B\u6240\u6709\u6587\u4EF6\u6458\u8981 (\u4F8B: @results/)`);
15509
+ console.log(` ${source_default.cyan("@*.csv")} \u901A\u914D\u7B26\u5185\u5D4C\u591A\u4E2A\u6587\u4EF6 (\u4F8B: @*.csv @*.tsv)`);
14796
15510
  console.log();
14797
15511
  console.log(source_default.bold.cyan("\u2500\u2500\u2500 \u5176\u4ED6 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
14798
15512
  console.log(` ${source_default.cyan("/help")} \u663E\u793A\u672C\u5E2E\u52A9`);
@@ -14884,10 +15598,10 @@ async function firstRunIfNeeded(rl) {
14884
15598
  function collectAllSkills() {
14885
15599
  const entries = [];
14886
15600
  const addFrom = (dir, tag) => {
14887
- if (!(0, import_fs4.existsSync)(dir)) return;
14888
- (0, import_fs4.readdirSync)(dir).forEach((f2) => {
15601
+ if (!(0, import_fs5.existsSync)(dir)) return;
15602
+ (0, import_fs5.readdirSync)(dir).forEach((f2) => {
14889
15603
  try {
14890
- if ((0, import_fs4.statSync)((0, import_path4.join)(dir, f2)).isDirectory()) entries.push({ id: f2, dir, tag });
15604
+ if ((0, import_fs5.statSync)((0, import_path5.join)(dir, f2)).isDirectory()) entries.push({ id: f2, dir, tag });
14891
15605
  } catch {
14892
15606
  }
14893
15607
  });
@@ -14915,7 +15629,72 @@ function listSkills(keyword) {
14915
15629
  if (matched.length > 50) console.log(source_default.dim(` ... \u8FD8\u6709 ${matched.length - 50} \u4E2A\uFF0C\u8BF7\u7528\u5173\u952E\u8BCD\u7B5B\u9009`));
14916
15630
  console.log();
14917
15631
  }
14918
- function injectSkill(id, history) {
15632
+ async function llmRecommendSkills(userQuery) {
15633
+ const cfg = loadConfig();
15634
+ const prov = PROVIDERS[cfg.provider];
15635
+ if (!prov) return [];
15636
+ const apiKey = cfg.apiKeys[cfg.provider] ?? (prov.envKey ? process.env[prov.envKey] : void 0);
15637
+ const requiresKey = prov.envKey !== "";
15638
+ if (requiresKey && !apiKey) return [];
15639
+ const baseURL = cfg.provider === "custom" ? cfg.customUrl ?? prov.baseURL : prov.baseURL;
15640
+ const model = cfg.provider === "custom" ? cfg.customModel ?? cfg.model : cfg.model;
15641
+ const all = collectAllSkills();
15642
+ const catalogLines = [];
15643
+ for (const entry of all) {
15644
+ const skillPath = (0, import_path5.join)(entry.dir, entry.id, "SKILL.md");
15645
+ if (!(0, import_fs5.existsSync)(skillPath)) continue;
15646
+ const raw = (0, import_fs5.readFileSync)(skillPath, "utf8");
15647
+ const { name, shortDesc } = parseSkillMeta(raw);
15648
+ const displayName = name || entry.id;
15649
+ const desc = shortDesc ? ` \u2014 ${shortDesc}` : "";
15650
+ catalogLines.push(`${entry.id}|${displayName}${desc}`);
15651
+ }
15652
+ if (catalogLines.length === 0) return [];
15653
+ const catalog = catalogLines.join("\n");
15654
+ const systemMsg = `\u4F60\u662F\u4E00\u4E2A\u751F\u7269\u4FE1\u606F\u5B66 Skill \u63A8\u8350\u52A9\u624B\u3002
15655
+ \u7528\u6237\u4F1A\u63CF\u8FF0\u4ED6\u4EEC\u60F3\u505A\u7684\u5206\u6790\u4EFB\u52A1\uFF0C\u4F60\u9700\u8981\u4ECE\u4E0B\u9762\u7684 Skill \u76EE\u5F55\u4E2D\u63A8\u8350\u6700\u76F8\u5173\u7684 5 \u4E2A\uFF08\u6216\u66F4\u5C11\uFF09\u3002
15656
+
15657
+ Skill \u76EE\u5F55\uFF08\u683C\u5F0F: id|\u540D\u79F0 \u2014 \u7B80\u4ECB\uFF09\uFF1A
15658
+ ${catalog}
15659
+
15660
+ \u8BF7\u4E25\u683C\u6309\u7167\u4EE5\u4E0B JSON \u683C\u5F0F\u56DE\u590D\uFF0C\u4E0D\u8981\u8F93\u51FA\u4EFB\u4F55\u5176\u4ED6\u5185\u5BB9\uFF1A
15661
+ [
15662
+ {"id": "skill-id", "name": "Skill \u540D\u79F0", "reason": "\u4E00\u53E5\u8BDD\u8BF4\u660E\u4E3A\u4EC0\u4E48\u63A8\u8350"},
15663
+ ...
15664
+ ]`;
15665
+ try {
15666
+ const client = new openai_default({ apiKey: apiKey || "none", baseURL });
15667
+ const resp = await client.chat.completions.create({
15668
+ model,
15669
+ messages: [
15670
+ { role: "system", content: systemMsg },
15671
+ { role: "user", content: userQuery }
15672
+ ],
15673
+ temperature: 0.2,
15674
+ max_tokens: 800
15675
+ });
15676
+ const text = resp.choices[0]?.message?.content ?? "";
15677
+ const jsonMatch = text.match(/\[[\s\S]*\]/);
15678
+ if (!jsonMatch) return [];
15679
+ const parsed = JSON.parse(jsonMatch[0]);
15680
+ const validIds = new Set(all.map((e2) => e2.id));
15681
+ return parsed.filter((r2) => r2.id && validIds.has(r2.id)).slice(0, 5);
15682
+ } catch {
15683
+ return [];
15684
+ }
15685
+ }
15686
+ function parseSkillMeta(content) {
15687
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/) || content.match(/<!--[\s\S]*?-->\s*([\s\S]*?)(?=\n#|\n\n)/);
15688
+ if (!fmMatch) return { name: "", shortDesc: "" };
15689
+ const fm = fmMatch[1];
15690
+ const nameMatch = fm.match(/^name:\s*['"]?(.+?)['"]?\s*$/m);
15691
+ const descMatch = fm.match(/^short-description:\s*(.+)$/m);
15692
+ return {
15693
+ name: nameMatch?.[1]?.trim() ?? "",
15694
+ shortDesc: descMatch?.[1]?.trim() ?? ""
15695
+ };
15696
+ }
15697
+ async function injectSkill(id, history, injectedSkills, rl, skipConfirm = false) {
14919
15698
  const all = collectAllSkills();
14920
15699
  const match = all.find((e2) => e2.id === id) || all.find((e2) => e2.id.startsWith(id)) || all.find((e2) => e2.id.includes(id));
14921
15700
  if (!match) {
@@ -14923,12 +15702,41 @@ function injectSkill(id, history) {
14923
15702
  console.log(source_default.dim("\u4F7F\u7528 /sk <\u5173\u952E\u8BCD> \u641C\u7D22"));
14924
15703
  return false;
14925
15704
  }
14926
- const skillPath = (0, import_path4.join)(match.dir, match.id, "SKILL.md");
14927
- if (!(0, import_fs4.existsSync)(skillPath)) {
15705
+ const skillPath = (0, import_path5.join)(match.dir, match.id, "SKILL.md");
15706
+ if (!(0, import_fs5.existsSync)(skillPath)) {
14928
15707
  console.log(source_default.red(`${match.id} \u7F3A\u5C11 SKILL.md`));
14929
15708
  return false;
14930
15709
  }
14931
- const content = (0, import_fs4.readFileSync)(skillPath, "utf8");
15710
+ const content = (0, import_fs5.readFileSync)(skillPath, "utf8");
15711
+ const { name, shortDesc } = parseSkillMeta(content);
15712
+ const displayName = name || match.id;
15713
+ if (!skipConfirm) {
15714
+ console.log();
15715
+ console.log(source_default.bold.cyan("\u250C\u2500 \u5373\u5C06\u52A0\u8F7D Skill \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
15716
+ console.log(`\u2502 ${source_default.bold("ID:")} ${source_default.cyan(match.id)}`);
15717
+ console.log(`\u2502 ${source_default.bold("\u540D\u79F0:")} ${displayName}`);
15718
+ if (shortDesc) {
15719
+ const words = shortDesc.split(" ");
15720
+ let line = "\u2502 \u529F\u80FD: ";
15721
+ for (const w2 of words) {
15722
+ if (line.length + w2.length > 70) {
15723
+ console.log(source_default.dim(line));
15724
+ line = "\u2502 " + w2 + " ";
15725
+ } else {
15726
+ line += w2 + " ";
15727
+ }
15728
+ }
15729
+ if (line.trim() !== "\u2502") console.log(source_default.dim(line));
15730
+ }
15731
+ console.log(source_default.bold.cyan("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
15732
+ console.log();
15733
+ const ans = await question(rl, source_default.cyan(" \u786E\u8BA4\u52A0\u8F7D\u6B64 Skill\uFF1F[Y/n] \u203A "));
15734
+ const confirmed = ans.trim() === "" || ans.trim().toLowerCase() === "y";
15735
+ if (!confirmed) {
15736
+ console.log(source_default.dim(" \u5DF2\u53D6\u6D88"));
15737
+ return false;
15738
+ }
15739
+ }
14932
15740
  history.push({
14933
15741
  role: "user",
14934
15742
  content: `[Skill \u5DF2\u52A0\u8F7D: ${match.id}]
@@ -14941,29 +15749,127 @@ ${content}`
14941
15749
  role: "assistant",
14942
15750
  content: `\u2713 Skill **${match.id}** \u5DF2\u52A0\u8F7D\u3002\u6211\u5DF2\u9605\u8BFB\u6307\u5357\uFF0C\u968F\u65F6\u53EF\u4EE5\u5F00\u59CB\u3002\u8BF7\u544A\u8BC9\u6211\u60A8\u7684\u5177\u4F53\u6570\u636E\u548C\u9700\u6C42\u3002`
14943
15751
  });
14944
- console.log(source_default.green(`\u2713 Skill ${match.id} \u5DF2\u6CE8\u5165\u5230\u5F53\u524D\u5BF9\u8BDD\u4E0A\u4E0B\u6587`));
15752
+ injectedSkills.set(match.id, displayName);
15753
+ console.log(source_default.green(`\u2713 Skill "${match.id}" \u5DF2\u52A0\u8F7D\u5230\u5F53\u524D\u5BF9\u8BDD\u4E0A\u4E0B\u6587`));
14945
15754
  return true;
14946
15755
  }
14947
- function expandFileRefs(input) {
14948
- return input.replace(/@"([^"]+)"|@'([^']+)'|@([\w./\\~:-]+)/g, (_2, q1, q2, q3) => {
14949
- const rawPath = q1 ?? q2 ?? q3;
14950
- try {
14951
- const resolved = (0, import_path4.resolve)(rawPath.replace(/^~/, (0, import_os3.homedir)()));
14952
- if (!(0, import_fs4.existsSync)(resolved)) return _2;
14953
- const content = (0, import_fs4.readFileSync)(resolved, "utf8");
14954
- const lines = content.split("\n");
14955
- const preview = lines.length > 100 ? lines.slice(0, 100).join("\n") + `
14956
- ... (\u5171 ${lines.length} \u884C\uFF0C\u5DF2\u622A\u65AD\u663E\u793A\u524D 100 \u884C)` : content;
15756
+ var FILE_SIZE_LIMIT = 100 * 1024;
15757
+ var DIR_FILE_LIMIT = 20;
15758
+ function listDirFiles(dirPath) {
15759
+ try {
15760
+ return (0, import_fs6.readdirSync)(dirPath).map((f2) => (0, import_path5.join)(dirPath, f2)).filter((p2) => {
15761
+ try {
15762
+ return (0, import_fs6.statSync)(p2).isFile();
15763
+ } catch {
15764
+ return false;
15765
+ }
15766
+ });
15767
+ } catch {
15768
+ return [];
15769
+ }
15770
+ }
15771
+ function expandSingleFile(resolved) {
15772
+ try {
15773
+ const stat = (0, import_fs6.statSync)(resolved);
15774
+ if (!stat.isFile()) return `[${resolved}: \u4E0D\u662F\u6587\u4EF6]`;
15775
+ if (stat.size > FILE_SIZE_LIMIT) {
14957
15776
  return `
14958
15777
  \`\`\`
14959
15778
  [\u6587\u4EF6: ${resolved}]
15779
+ (\u6587\u4EF6\u8FC7\u5927 ${Math.round(stat.size / 1024)}KB\uFF0C\u5DF2\u8DF3\u8FC7)
15780
+ \`\`\`
15781
+ `;
15782
+ }
15783
+ const content = (0, import_fs5.readFileSync)(resolved, "utf8");
15784
+ const lines = content.split("\n");
15785
+ const preview = lines.length > 150 ? lines.slice(0, 150).join("\n") + `
15786
+ ... (\u5171 ${lines.length} \u884C\uFF0C\u5DF2\u622A\u65AD)` : content;
15787
+ return `
15788
+ \`\`\`
15789
+ [\u6587\u4EF6: ${resolved}]
14960
15790
  ${preview}
14961
15791
  \`\`\`
14962
15792
  `;
15793
+ } catch {
15794
+ return `[\u65E0\u6CD5\u8BFB\u53D6: ${resolved}]`;
15795
+ }
15796
+ }
15797
+ function expandFileRefs(input) {
15798
+ return input.replace(/@"([^"]+)"|@'([^']+)'|@([\/\w.*?~:-]+)/g, (match, q1, q2, q3) => {
15799
+ const rawPath = q1 ?? q2 ?? q3;
15800
+ const expanded = rawPath.replace(/^~/, (0, import_os3.homedir)());
15801
+ if (rawPath.endsWith("/") || rawPath.endsWith("\\")) {
15802
+ const dirResolved = (0, import_path5.resolve)(expanded);
15803
+ if (!(0, import_fs5.existsSync)(dirResolved)) return match;
15804
+ const files = listDirFiles(dirResolved).slice(0, DIR_FILE_LIMIT);
15805
+ if (files.length === 0) return `[\u76EE\u5F55 ${dirResolved} \u4E3A\u7A7A]`;
15806
+ const parts = [`
15807
+ [\u76EE\u5F55: ${dirResolved} \u5171 ${files.length} \u4E2A\u6587\u4EF6]`];
15808
+ for (const f2 of files) parts.push(expandSingleFile(f2));
15809
+ return parts.join("\n");
15810
+ }
15811
+ if (rawPath.includes("*") || rawPath.includes("?")) {
15812
+ const { globSync } = (() => {
15813
+ try {
15814
+ return require("glob");
15815
+ } catch {
15816
+ return { globSync: null };
15817
+ }
15818
+ })();
15819
+ let matched = [];
15820
+ if (globSync) {
15821
+ matched = globSync(expanded, { absolute: true }).slice(0, DIR_FILE_LIMIT);
15822
+ } else {
15823
+ const ext = rawPath.replace(/^.*\./, ".");
15824
+ matched = listDirFiles(process.cwd()).filter((f2) => f2.endsWith(ext)).slice(0, DIR_FILE_LIMIT);
15825
+ }
15826
+ if (matched.length === 0) return `[\u672A\u627E\u5230\u5339\u914D: ${rawPath}]`;
15827
+ const parts = [`
15828
+ [\u901A\u914D\u7B26\u5339\u914D: ${rawPath} \u5171 ${matched.length} \u4E2A\u6587\u4EF6]`];
15829
+ for (const f2 of matched) parts.push(expandSingleFile(f2));
15830
+ return parts.join("\n");
15831
+ }
15832
+ const resolved = (0, import_path5.resolve)(expanded);
15833
+ if (!(0, import_fs5.existsSync)(resolved)) return match;
15834
+ return expandSingleFile(resolved);
15835
+ });
15836
+ }
15837
+ function snapshotWorkdir(dir) {
15838
+ const snap = /* @__PURE__ */ new Map();
15839
+ function walk(d2, depth = 0) {
15840
+ if (depth > 3) return;
15841
+ try {
15842
+ for (const entry of (0, import_fs5.readdirSync)(d2)) {
15843
+ if (entry.startsWith(".") || entry === "node_modules") continue;
15844
+ const full = (0, import_path5.join)(d2, entry);
15845
+ try {
15846
+ const st2 = (0, import_fs5.statSync)(full);
15847
+ if (st2.isDirectory()) {
15848
+ walk(full, depth + 1);
15849
+ } else {
15850
+ snap.set(full, { path: full, mtime: st2.mtimeMs, size: st2.size });
15851
+ }
15852
+ } catch {
15853
+ }
15854
+ }
14963
15855
  } catch {
14964
- return _2;
14965
15856
  }
14966
- });
15857
+ }
15858
+ walk(dir);
15859
+ return snap;
15860
+ }
15861
+ function diffWorkdir(before, after) {
15862
+ const added = [];
15863
+ const modified = [];
15864
+ for (const [path, snap] of after) {
15865
+ const prev = before.get(path);
15866
+ if (!prev) {
15867
+ added.push(path);
15868
+ } else if (snap.mtime !== prev.mtime || snap.size !== prev.size) {
15869
+ modified.push(path);
15870
+ }
15871
+ }
15872
+ return { added, modified };
14967
15873
  }
14968
15874
  function saveConversation(history, filename) {
14969
15875
  if (history.length === 0) {
@@ -14972,7 +15878,7 @@ function saveConversation(history, filename) {
14972
15878
  }
14973
15879
  const now = /* @__PURE__ */ new Date();
14974
15880
  const stamp = now.toISOString().slice(0, 16).replace("T", "-").replace(":", "-");
14975
- const outPath = (0, import_path4.resolve)(filename || `bgicli-chat-${stamp}.md`);
15881
+ const outPath = (0, import_path5.resolve)(filename || `bgicli-chat-${stamp}.md`);
14976
15882
  const lines = [`# BGI CLI \u5BF9\u8BDD\u8BB0\u5F55
14977
15883
  `, `> \u5BFC\u51FA\u65F6\u95F4: ${now.toLocaleString("zh-CN")}
14978
15884
  `];
@@ -14991,24 +15897,41 @@ ${msg.content}
14991
15897
  `);
14992
15898
  }
14993
15899
  }
14994
- (0, import_fs4.writeFileSync)(outPath, lines.join("\n"), "utf8");
15900
+ (0, import_fs5.writeFileSync)(outPath, lines.join("\n"), "utf8");
14995
15901
  console.log(source_default.green(`\u2713 \u5BF9\u8BDD\u5DF2\u4FDD\u5B58: ${outPath}`));
14996
15902
  }
14997
- var COMPACT_TOKEN_THRESHOLD = 6e4;
15903
+ var COMPACT_TOKEN_THRESHOLD = 4e4;
14998
15904
  var COMPACT_KEEP_RECENT = 8;
14999
- function estimateTokens(messages) {
15000
- const chars = messages.reduce((n2, m2) => n2 + String(m2.content ?? "").length, 0);
15001
- return Math.round(chars / 3.5);
15002
- }
15905
+ var WARN_TOKEN_THRESHOLD = 3e4;
15906
+ var estimateTokens2 = estimateTokens;
15003
15907
  async function maybeCompact(history, cfg) {
15004
- const tokens = estimateTokens(history);
15005
- if (tokens < COMPACT_TOKEN_THRESHOLD) return history;
15006
- const recent = history.slice(-COMPACT_KEEP_RECENT);
15007
- const old = history.slice(0, -COMPACT_KEEP_RECENT);
15008
- if (old.length === 0) return history;
15009
- process.stdout.write(source_default.dim(`
15010
- [\u4E0A\u4E0B\u6587\u5DF2\u8FBE ~${Math.round(tokens / 1e3)}k tokens\uFF0C\u6B63\u5728\u81EA\u52A8\u538B\u7F29...]
15011
- `));
15908
+ let cleaned = deduplicateSkillInjections(trimToolOutputs(history));
15909
+ const tokensBefore = estimateTokens2(history);
15910
+ const tokensAfter = estimateTokens2(cleaned);
15911
+ if (tokensAfter < tokensBefore) {
15912
+ const saved = tokensBefore - tokensAfter;
15913
+ process.stdout.write(source_default.dim(
15914
+ `
15915
+ [\u4E0A\u4E0B\u6587\u4F18\u5316: \u53BB\u91CD/\u622A\u65AD\u8282\u7701 ~${Math.round(saved / 1e3)}k tokens]
15916
+ `
15917
+ ));
15918
+ }
15919
+ if (tokensAfter >= WARN_TOKEN_THRESHOLD && tokensAfter < COMPACT_TOKEN_THRESHOLD) {
15920
+ process.stdout.write(source_default.dim(
15921
+ `[\u63D0\u793A: \u4E0A\u4E0B\u6587\u5DF2\u8FBE ~${Math.round(tokensAfter / 1e3)}k tokens\uFF0C\u63A5\u8FD1\u538B\u7F29\u9608\u503C ${Math.round(COMPACT_TOKEN_THRESHOLD / 1e3)}k]
15922
+ `
15923
+ ));
15924
+ return cleaned;
15925
+ }
15926
+ if (tokensAfter < COMPACT_TOKEN_THRESHOLD) return cleaned;
15927
+ const recent = cleaned.slice(-COMPACT_KEEP_RECENT);
15928
+ const old = cleaned.slice(0, -COMPACT_KEEP_RECENT);
15929
+ if (old.length === 0) return cleaned;
15930
+ process.stdout.write(source_default.dim(
15931
+ `
15932
+ [\u4E0A\u4E0B\u6587\u5DF2\u8FBE ~${Math.round(tokensAfter / 1e3)}k tokens\uFF0C\u6B63\u5728\u81EA\u52A8\u538B\u7F29\u5386\u53F2...]
15933
+ `
15934
+ ));
15012
15935
  try {
15013
15936
  const summary = await compactMessages(old, cfg);
15014
15937
  const compacted = [
@@ -15024,16 +15947,31 @@ ${summary}`
15024
15947
  },
15025
15948
  ...recent
15026
15949
  ];
15027
- const saved = estimateTokens(history) - estimateTokens(compacted);
15028
- process.stdout.write(source_default.dim(`[\u538B\u7F29\u5B8C\u6210\uFF0C\u91CA\u653E\u7EA6 ~${Math.round(saved / 1e3)}k tokens]
15950
+ const finalTokens = estimateTokens2(compacted);
15951
+ const totalSaved = tokensBefore - finalTokens;
15952
+ process.stdout.write(source_default.dim(
15953
+ `[\u538B\u7F29\u5B8C\u6210: ~${Math.round(tokensBefore / 1e3)}k \u2192 ~${Math.round(finalTokens / 1e3)}k tokens\uFF0C\u8282\u7701 ~${Math.round(totalSaved / 1e3)}k]
15029
15954
 
15030
- `));
15955
+ `
15956
+ ));
15031
15957
  return compacted;
15032
15958
  } catch {
15033
- return history.slice(-COMPACT_KEEP_RECENT * 2);
15034
- }
15035
- }
15036
- async function handleCommand(input, rl, history, thinkMode) {
15959
+ process.stdout.write(source_default.yellow("[\u538B\u7F29\u5931\u8D25\uFF0C\u56DE\u9000\u5230\u622A\u65AD\u6A21\u5F0F]\n"));
15960
+ return cleaned.slice(-COMPACT_KEEP_RECENT * 2);
15961
+ }
15962
+ }
15963
+ function formatAge(isoDate) {
15964
+ const diff = Date.now() - new Date(isoDate).getTime();
15965
+ const mins = Math.floor(diff / 6e4);
15966
+ const hours = Math.floor(diff / 36e5);
15967
+ const days = Math.floor(diff / 864e5);
15968
+ if (mins < 1) return "\u521A\u521A";
15969
+ if (mins < 60) return `${mins} \u5206\u949F\u524D`;
15970
+ if (hours < 24) return `${hours} \u5C0F\u65F6\u524D`;
15971
+ if (days < 30) return `${days} \u5929\u524D`;
15972
+ return new Date(isoDate).toLocaleDateString("zh-CN");
15973
+ }
15974
+ async function handleCommand(input, rl, history, thinkMode, injectedSkills) {
15037
15975
  const [cmd, ...rest] = input.slice(1).trim().split(/\s+/);
15038
15976
  const arg = rest.join(" ");
15039
15977
  const cfg = loadConfig();
@@ -15124,20 +16062,370 @@ async function handleCommand(input, rl, history, thinkMode) {
15124
16062
  return { clearHistory: true };
15125
16063
  case "history": {
15126
16064
  const turns = Math.floor(history.length / 2);
15127
- const chars = history.reduce((n2, m2) => n2 + String(m2.content ?? "").length, 0);
15128
- const estTokens = Math.round(chars / 3.5);
16065
+ const estTokens = estimateTokens2(history);
16066
+ const pct = Math.round(estTokens / COMPACT_TOKEN_THRESHOLD * 100);
16067
+ const bar = "\u2588".repeat(Math.min(20, Math.round(pct / 5))) + "\u2591".repeat(Math.max(0, 20 - Math.round(pct / 5)));
15129
16068
  console.log(source_default.bold("\u5BF9\u8BDD\u7EDF\u8BA1:"));
15130
16069
  console.log(` \u8F6E\u6B21: ${turns}`);
15131
16070
  console.log(` \u6D88\u606F\u603B\u6570: ${history.length}`);
15132
16071
  console.log(` \u4F30\u7B97 Token: ~${estTokens.toLocaleString()}`);
16072
+ console.log(` \u4E0A\u4E0B\u6587\u7528\u91CF: [${pct >= 80 ? source_default.red(bar) : pct >= 50 ? source_default.yellow(bar) : source_default.green(bar)}] ${pct}%`);
16073
+ console.log(` \u538B\u7F29\u9608\u503C: ~${Math.round(COMPACT_TOKEN_THRESHOLD / 1e3)}k tokens`);
16074
+ if (pct >= 80) console.log(source_default.yellow(" \u26A0 \u63A5\u8FD1\u4E0A\u9650\uFF0C\u5EFA\u8BAE\u8FD0\u884C /compact"));
15133
16075
  break;
15134
16076
  }
15135
16077
  case "save": {
15136
16078
  saveConversation(history, arg || void 0);
15137
16079
  break;
15138
16080
  }
16081
+ // ── Session persistence ──────────────────────────────────────────────────
16082
+ case "sessions": {
16083
+ const sessions = listSessions();
16084
+ if (sessions.length === 0) {
16085
+ console.log(source_default.dim("\u6682\u65E0\u5386\u53F2\u4F1A\u8BDD\u3002\u5BF9\u8BDD\u7ED3\u675F\u540E\u4F1A\u81EA\u52A8\u4FDD\u5B58\u3002"));
16086
+ break;
16087
+ }
16088
+ console.log(source_default.bold(`
16089
+ \u5386\u53F2\u4F1A\u8BDD (${sessions.length} \u4E2A):
16090
+ `));
16091
+ sessions.slice(0, 20).forEach((s2, i2) => {
16092
+ const age = formatAge(s2.updatedAt);
16093
+ const skills = s2.skills.length > 0 ? source_default.dim(` [${s2.skills.join(", ")}]`) : "";
16094
+ const preview = s2.preview ? source_default.dim(` \u2014 "${s2.preview.slice(0, 40)}${s2.preview.length > 40 ? "\u2026" : ""}"`) : "";
16095
+ const marker = i2 === 0 ? source_default.green("\u25CF") : source_default.dim("\u25CB");
16096
+ console.log(` ${marker} ${source_default.cyan(s2.id)} ${source_default.dim(age)}${skills}`);
16097
+ if (preview) console.log(` ${preview}`);
16098
+ });
16099
+ console.log();
16100
+ console.log(source_default.dim("\u4F7F\u7528 /resume [id] \u6062\u590D\u4F1A\u8BDD /session-del <id> \u5220\u9664"));
16101
+ break;
16102
+ }
16103
+ case "resume": {
16104
+ let targetId = arg;
16105
+ if (!targetId) {
16106
+ const last = getLastSession();
16107
+ if (!last) {
16108
+ console.log(source_default.yellow("\u6682\u65E0\u5386\u53F2\u4F1A\u8BDD"));
16109
+ break;
16110
+ }
16111
+ targetId = last.id;
16112
+ console.log(source_default.dim(`\u6062\u590D\u6700\u8FD1\u4F1A\u8BDD: ${targetId}`));
16113
+ }
16114
+ const session = loadSession(targetId);
16115
+ if (!session) {
16116
+ const all = listSessions();
16117
+ const match = all.find((s3) => s3.id.includes(targetId));
16118
+ if (!match) {
16119
+ console.log(source_default.red(`\u672A\u627E\u5230\u4F1A\u8BDD: ${targetId}`));
16120
+ break;
16121
+ }
16122
+ const s2 = loadSession(match.id);
16123
+ if (!s2) {
16124
+ console.log(source_default.red("\u52A0\u8F7D\u5931\u8D25"));
16125
+ break;
16126
+ }
16127
+ console.log(source_default.green(`\u2713 \u5DF2\u6062\u590D\u4F1A\u8BDD ${s2.id} (${s2.messageCount} \u6761\u6D88\u606F)`));
16128
+ if (s2.skills.length > 0) console.log(source_default.dim(` \u5DF2\u52A0\u8F7D Skills: ${s2.skills.join(", ")}`));
16129
+ for (const sk of s2.skills) injectedSkills.set(sk, sk);
16130
+ return { injectHistory: s2.messages };
16131
+ }
16132
+ console.log(source_default.green(`\u2713 \u5DF2\u6062\u590D\u4F1A\u8BDD ${session.id} (${session.messageCount} \u6761\u6D88\u606F)`));
16133
+ if (session.skills.length > 0) console.log(source_default.dim(` \u5DF2\u52A0\u8F7D Skills: ${session.skills.join(", ")}`));
16134
+ for (const sk of session.skills) injectedSkills.set(sk, sk);
16135
+ return { injectHistory: session.messages };
16136
+ }
16137
+ case "session-save": {
16138
+ const sessionId = SESSION_CTX.id || void 0;
16139
+ if (!sessionId) {
16140
+ console.log(source_default.yellow("\u5F53\u524D\u4F1A\u8BDD\u5C1A\u672A\u521D\u59CB\u5316"));
16141
+ break;
16142
+ }
16143
+ const name = arg || (/* @__PURE__ */ new Date()).toLocaleString("zh-CN");
16144
+ saveSession(sessionId, name, history, Array.from(injectedSkills.keys()), SESSION_CTX.createdAt || (/* @__PURE__ */ new Date()).toISOString());
16145
+ console.log(source_default.green(`\u2713 \u4F1A\u8BDD\u5DF2\u4FDD\u5B58: ${sessionId} \u540D\u79F0: "${name}"`));
16146
+ break;
16147
+ }
16148
+ case "session-del": {
16149
+ if (!arg) {
16150
+ console.log("\u7528\u6CD5: /session-del <id>");
16151
+ break;
16152
+ }
16153
+ const ok = deleteSession(arg);
16154
+ console.log(ok ? source_default.green(`\u2713 \u5DF2\u5220\u9664\u4F1A\u8BDD: ${arg}`) : source_default.yellow(`\u672A\u627E\u5230\u4F1A\u8BDD: ${arg}`));
16155
+ break;
16156
+ }
16157
+ // ── Checkpoints ──────────────────────────────────────────────────────────
16158
+ case "checkpoint": {
16159
+ const sessionId = SESSION_CTX.id || "default";
16160
+ const sub = arg.split(/\s+/)[0]?.toLowerCase();
16161
+ const subArg = arg.split(/\s+/).slice(1).join(" ");
16162
+ if (!sub || sub === "save" || sub === "") {
16163
+ const label = subArg || `\u7B2C ${Math.floor(history.length / 2)} \u8F6E`;
16164
+ const cpId = saveCheckpoint(sessionId, label, history, Array.from(injectedSkills.keys()));
16165
+ console.log(source_default.green(`\u2713 \u65AD\u70B9\u5DF2\u4FDD\u5B58: ${cpId} \u6807\u7B7E: "${label}"`));
16166
+ } else if (sub === "list") {
16167
+ const cps = listCheckpoints(sessionId);
16168
+ if (cps.length === 0) {
16169
+ console.log(source_default.dim("\u5F53\u524D\u4F1A\u8BDD\u6682\u65E0\u65AD\u70B9"));
16170
+ } else {
16171
+ console.log(source_default.bold(`
16172
+ \u5F53\u524D\u4F1A\u8BDD\u65AD\u70B9 (${cps.length} \u4E2A):
16173
+ `));
16174
+ cps.forEach((cp) => {
16175
+ const age = formatAge(cp.createdAt);
16176
+ console.log(` ${source_default.cyan(cp.id.split("-cp")[1] ?? cp.id)} ${source_default.dim(age)} "${cp.label}" (${cp.messageCount} \u6761\u6D88\u606F)`);
16177
+ console.log(source_default.dim(` /checkpoint restore ${cp.id}`));
16178
+ });
16179
+ }
16180
+ } else if (sub === "restore") {
16181
+ if (!subArg) {
16182
+ console.log("\u7528\u6CD5: /checkpoint restore <id>");
16183
+ break;
16184
+ }
16185
+ const cps = listCheckpoints(sessionId);
16186
+ const target = cps.find((cp) => cp.id === subArg || cp.id.endsWith(subArg));
16187
+ if (!target) {
16188
+ console.log(source_default.red(`\u672A\u627E\u5230\u65AD\u70B9: ${subArg}`));
16189
+ break;
16190
+ }
16191
+ console.log(source_default.green(`\u2713 \u5DF2\u6062\u590D\u5230\u65AD\u70B9: "${target.label}" (${target.messageCount} \u6761\u6D88\u606F)`));
16192
+ injectedSkills.clear();
16193
+ for (const sk of target.skills) injectedSkills.set(sk, sk);
16194
+ return { injectHistory: target.messages };
16195
+ } else if (sub === "clear") {
16196
+ const n2 = clearCheckpoints(sessionId);
16197
+ console.log(source_default.green(`\u2713 \u5DF2\u6E05\u9664 ${n2} \u4E2A\u65AD\u70B9`));
16198
+ } else {
16199
+ console.log("\u7528\u6CD5: /checkpoint [list|restore <id>|clear]");
16200
+ }
16201
+ break;
16202
+ }
16203
+ // ── Workdir diff ─────────────────────────────────────────────────────────
16204
+ case "diff": {
16205
+ const snap = SESSION_CTX.wdirSnapshot ?? void 0;
16206
+ if (!snap) {
16207
+ console.log(source_default.dim("\u5DE5\u4F5C\u76EE\u5F55\u5FEB\u7167\u5C1A\u672A\u5EFA\u7ACB\uFF08\u4F1A\u8BDD\u5F00\u59CB\u65F6\u81EA\u52A8\u521B\u5EFA\uFF09"));
16208
+ break;
16209
+ }
16210
+ const current = snapshotWorkdir(process.cwd());
16211
+ const { added, modified } = diffWorkdir(snap, current);
16212
+ if (added.length === 0 && modified.length === 0) {
16213
+ console.log(source_default.dim("\u672C\u6B21\u4F1A\u8BDD\u672A\u4EA7\u751F\u65B0\u6587\u4EF6\u6216\u4FEE\u6539"));
16214
+ } else {
16215
+ if (added.length > 0) {
16216
+ console.log(source_default.bold.green(`
16217
+ \u65B0\u589E\u6587\u4EF6 (${added.length} \u4E2A):`));
16218
+ added.forEach((f2) => console.log(` ${source_default.green("+")} ${f2}`));
16219
+ }
16220
+ if (modified.length > 0) {
16221
+ console.log(source_default.bold.yellow(`
16222
+ \u4FEE\u6539\u6587\u4EF6 (${modified.length} \u4E2A):`));
16223
+ modified.forEach((f2) => console.log(` ${source_default.yellow("~")} ${f2}`));
16224
+ }
16225
+ console.log();
16226
+ }
16227
+ break;
16228
+ }
16229
+ // ── /run workflow wizard ─────────────────────────────────────────────────
16230
+ case "run": {
16231
+ const targetId = arg;
16232
+ if (!targetId) {
16233
+ console.log("\u7528\u6CD5: /run <skill-id>");
16234
+ console.log(source_default.dim("\u793A\u4F8B: /run deseq2-analysis /run survival-analysis-clinical"));
16235
+ break;
16236
+ }
16237
+ const allSkillsRun = collectAllSkills();
16238
+ const runMatch = allSkillsRun.find((e2) => e2.id === targetId || e2.id.startsWith(targetId) || e2.id.includes(targetId));
16239
+ if (!runMatch) {
16240
+ console.log(source_default.red(`\u672A\u627E\u5230 Skill: ${targetId}`));
16241
+ break;
16242
+ }
16243
+ const skillPath = (0, import_path5.join)(runMatch.dir, runMatch.id, "SKILL.md");
16244
+ if (!(0, import_fs5.existsSync)(skillPath)) {
16245
+ console.log(source_default.red(`${runMatch.id} \u7F3A\u5C11 SKILL.md`));
16246
+ break;
16247
+ }
16248
+ const skillContent = (0, import_fs5.readFileSync)(skillPath, "utf8");
16249
+ const { name: skillName } = parseSkillMeta(skillContent);
16250
+ const paramsMatch = skillContent.match(/##\s*(?:必要参数|Required Parameters|参数)[\s\S]*?(?=\n##|$)/i);
16251
+ const paramsSection = paramsMatch?.[0] ?? "";
16252
+ console.log(source_default.bold.cyan(`
16253
+ \u2500\u2500\u2500 /run ${runMatch.id} \u5411\u5BFC \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
16254
+ console.log(source_default.dim(` Skill: ${skillName || runMatch.id}`));
16255
+ console.log(source_default.dim(" \u8BF7\u56DE\u7B54\u4EE5\u4E0B\u95EE\u9898\uFF0CAI \u5C06\u81EA\u52A8\u751F\u6210\u5E76\u6267\u884C\u5206\u6790\u811A\u672C\n"));
16256
+ const paramLines = paramsSection.split("\n").filter((l2) => /^[-*•]|^\d+\./.test(l2.trim())).map((l2) => l2.replace(/^[-*•\d.]+\s*/, "").trim()).filter(Boolean).slice(0, 8);
16257
+ const questions = paramLines.length > 0 ? paramLines : [
16258
+ "\u6570\u636E\u6587\u4EF6\u8DEF\u5F84\uFF08\u652F\u6301\u76F8\u5BF9\u8DEF\u5F84\uFF0C\u5982 ./data/counts.csv\uFF09",
16259
+ "\u6837\u672C\u5206\u7EC4\u4FE1\u606F\uFF08\u5982 treatment,treatment,control,control\uFF09",
16260
+ "\u5BF9\u7167\u7EC4\u540D\u79F0",
16261
+ "\u5B9E\u9A8C\u7EC4\u540D\u79F0",
16262
+ "\u8F93\u51FA\u76EE\u5F55\uFF08\u9ED8\u8BA4: ./results\uFF09"
16263
+ ];
16264
+ const answers = {};
16265
+ for (const q2 of questions) {
16266
+ const ans = await question(rl, source_default.blue(` ${q2}: `));
16267
+ if (ans.trim()) answers[q2] = ans.trim();
16268
+ }
16269
+ const paramSummary = Object.entries(answers).map(([k2, v2]) => `- ${k2}: ${v2}`).join("\n");
16270
+ const runPrompt = `\u8BF7\u6839\u636E\u4EE5\u4E0B\u53C2\u6570\uFF0C\u4F7F\u7528 ${skillName || runMatch.id} \u5DE5\u4F5C\u6D41\u751F\u6210\u5B8C\u6574\u7684\u5206\u6790\u811A\u672C\u5E76\u7ACB\u5373\u6267\u884C\uFF1A
16271
+
16272
+ \u7528\u6237\u63D0\u4F9B\u7684\u53C2\u6570\uFF1A
16273
+ ${paramSummary}
16274
+
16275
+ \u8BF7\uFF1A
16276
+ 1. \u751F\u6210\u5B8C\u6574\u53EF\u8FD0\u884C\u7684\u5206\u6790\u811A\u672C\uFF08R \u6216 Python\uFF09
16277
+ 2. \u4F7F\u7528 bash \u5DE5\u5177\u6267\u884C\u811A\u672C
16278
+ 3. \u5206\u6790\u5B8C\u6210\u540E\u7ED9\u51FA\u7ED3\u679C\u6458\u8981`;
16279
+ if (!injectedSkills.has(runMatch.id)) {
16280
+ await injectSkill(runMatch.id, history, injectedSkills, rl, true);
16281
+ }
16282
+ history.push({ role: "user", content: runPrompt });
16283
+ console.log(source_default.dim("\n \u6B63\u5728\u751F\u6210\u5E76\u6267\u884C\u5206\u6790\u811A\u672C...\n"));
16284
+ try {
16285
+ const runCfg = loadConfig();
16286
+ const reply = await chat(history, runCfg, buildSystemPrompt());
16287
+ history.push({ role: "assistant", content: reply });
16288
+ } catch (err) {
16289
+ console.error(source_default.red(`\u6267\u884C\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`));
16290
+ history.pop();
16291
+ }
16292
+ break;
16293
+ }
16294
+ // ── /check-env ───────────────────────────────────────────────────────────
16295
+ case "check-env": {
16296
+ const checkId = arg;
16297
+ const skillsToCheck = checkId ? (() => {
16298
+ const all = collectAllSkills();
16299
+ const m2 = all.find((e2) => e2.id === checkId || e2.id.startsWith(checkId) || e2.id.includes(checkId));
16300
+ return m2 ? [m2] : [];
16301
+ })() : collectAllSkills().filter((e2) => injectedSkills.has(e2.id));
16302
+ if (skillsToCheck.length === 0) {
16303
+ console.log(source_default.yellow(checkId ? `\u672A\u627E\u5230 Skill: ${checkId}` : "\u5F53\u524D\u4F1A\u8BDD\u672A\u52A0\u8F7D\u4EFB\u4F55 Skill"));
16304
+ break;
16305
+ }
16306
+ for (const entry of skillsToCheck) {
16307
+ const sp = (0, import_path5.join)(entry.dir, entry.id, "SKILL.md");
16308
+ if (!(0, import_fs5.existsSync)(sp)) continue;
16309
+ const content = (0, import_fs5.readFileSync)(sp, "utf8");
16310
+ const { name } = parseSkillMeta(content);
16311
+ console.log(source_default.bold(`
16312
+ \u68C0\u6D4B ${name || entry.id} \u7684\u4F9D\u8D56\u73AF\u5883:`));
16313
+ const rPkgs = [.../* @__PURE__ */ new Set([
16314
+ ...(content.match(/library\(([\w.]+)\)/g) ?? []).map((m2) => m2.replace(/library\(|\)/g, "")),
16315
+ ...(content.match(/require\(([\w.]+)\)/g) ?? []).map((m2) => m2.replace(/require\(|\)/g, "")),
16316
+ ...(content.match(/BiocManager::install\("([^"]+)"\)/g) ?? []).map((m2) => m2.replace(/BiocManager::install\("|"\)/g, ""))
16317
+ ])];
16318
+ const pyPkgs = [.../* @__PURE__ */ new Set([
16319
+ ...(content.match(/import (\w+)/g) ?? []).map((m2) => m2.replace("import ", "")),
16320
+ ...(content.match(/from (\w+) import/g) ?? []).map((m2) => m2.replace("from ", "").replace(" import", ""))
16321
+ ])];
16322
+ if (rPkgs.length > 0) {
16323
+ console.log(source_default.dim(` R \u5305 (${rPkgs.length} \u4E2A): ${rPkgs.join(", ")}`));
16324
+ const checkScript = rPkgs.map(
16325
+ (p2) => `cat(sprintf(" %-30s %s\\n", "${p2}", if(requireNamespace("${p2}", quietly=TRUE)) "\u2713 \u5DF2\u5B89\u88C5" else "\u2717 \u672A\u5B89\u88C5"))`
16326
+ ).join("\n");
16327
+ const result = await executeTool("bash", { command: `Rscript -e '${checkScript}'` });
16328
+ if (result.error) {
16329
+ console.log(source_default.dim(" (R \u672A\u5B89\u88C5\u6216\u4E0D\u53EF\u7528)"));
16330
+ } else {
16331
+ console.log(result.output);
16332
+ const missing = rPkgs.filter((p2) => result.output.includes(`${p2}`) && result.output.includes("\u2717"));
16333
+ if (missing.length > 0) {
16334
+ console.log(source_default.yellow(` \u7F3A\u5C11 ${missing.length} \u4E2A R \u5305\uFF0C\u5B89\u88C5\u547D\u4EE4:`));
16335
+ console.log(source_default.cyan(` !Rscript -e 'install.packages(c(${missing.map((p2) => `"${p2}"`).join(", ")}))'`));
16336
+ const biocPkgs = missing.filter((p2) => ["DESeq2", "edgeR", "limma", "clusterProfiler", "Seurat", "SingleCellExperiment", "BiocGenerics"].includes(p2));
16337
+ if (biocPkgs.length > 0) {
16338
+ console.log(source_default.cyan(` !Rscript -e 'BiocManager::install(c(${biocPkgs.map((p2) => `"${p2}"`).join(", ")}))'`));
16339
+ }
16340
+ }
16341
+ }
16342
+ }
16343
+ if (pyPkgs.length > 0) {
16344
+ const commonPy = ["numpy", "pandas", "scipy", "sklearn", "matplotlib", "seaborn", "scanpy", "anndata", "torch"];
16345
+ const relevantPy = pyPkgs.filter((p2) => commonPy.includes(p2));
16346
+ if (relevantPy.length > 0) {
16347
+ console.log(source_default.dim(` Python \u5305 (${relevantPy.length} \u4E2A): ${relevantPy.join(", ")}`));
16348
+ const checkPy = relevantPy.map(
16349
+ (p2) => `python3 -c "import ${p2}; print(' %-30s \u2713 \u5DF2\u5B89\u88C5' % '${p2}')" 2>/dev/null || echo " ${p2.padEnd(30)} \u2717 \u672A\u5B89\u88C5"`
16350
+ ).join(" && ");
16351
+ const result = await executeTool("bash", { command: checkPy });
16352
+ if (!result.error) console.log(result.output);
16353
+ }
16354
+ }
16355
+ if (rPkgs.length === 0 && pyPkgs.length === 0) {
16356
+ console.log(source_default.dim(" \u672A\u68C0\u6D4B\u5230\u660E\u786E\u7684\u5305\u4F9D\u8D56\u58F0\u660E"));
16357
+ }
16358
+ }
16359
+ break;
16360
+ }
16361
+ // ── /install from GitHub ─────────────────────────────────────────────────
16362
+ case "install": {
16363
+ if (!arg) {
16364
+ console.log("\u7528\u6CD5: /install <github-url>");
16365
+ console.log(source_default.dim("\u793A\u4F8B: /install https://github.com/user/my-skill"));
16366
+ console.log(source_default.dim(" /install user/repo (GitHub \u7B80\u5199)"));
16367
+ break;
16368
+ }
16369
+ let repoUrl = arg;
16370
+ if (!repoUrl.startsWith("http")) {
16371
+ repoUrl = `https://github.com/${repoUrl}`;
16372
+ }
16373
+ const repoName = repoUrl.replace(/\.git$/, "").split("/").pop() ?? "unknown-skill";
16374
+ const installTarget = (0, import_path5.join)(SKILLS_DIR, repoName);
16375
+ if ((0, import_fs5.existsSync)(installTarget)) {
16376
+ console.log(source_default.yellow(`Skill "${repoName}" \u5DF2\u5B58\u5728\uFF0C\u5982\u9700\u66F4\u65B0\u8BF7\u5148 /uninstall ${repoName}`));
16377
+ break;
16378
+ }
16379
+ console.log(source_default.dim(`\u6B63\u5728\u4ECE GitHub \u5B89\u88C5 Skill: ${repoName}...`));
16380
+ const cloneResult = await executeTool("bash", {
16381
+ command: `git clone --depth 1 "${repoUrl}" "${installTarget}" 2>&1`
16382
+ });
16383
+ if (cloneResult.error || cloneResult.output.includes("fatal:")) {
16384
+ console.log(source_default.red(`\u5B89\u88C5\u5931\u8D25: ${cloneResult.output || cloneResult.error}`));
16385
+ break;
16386
+ }
16387
+ const skillMdPath = (0, import_path5.join)(installTarget, "SKILL.md");
16388
+ if (!(0, import_fs5.existsSync)(skillMdPath)) {
16389
+ console.log(source_default.red(`\u5B89\u88C5\u5931\u8D25: ${repoName} \u7F3A\u5C11 SKILL.md \u6587\u4EF6`));
16390
+ await executeTool("bash", { command: `rm -rf "${installTarget}"` });
16391
+ break;
16392
+ }
16393
+ const content = (0, import_fs5.readFileSync)(skillMdPath, "utf8");
16394
+ const { name, shortDesc } = parseSkillMeta(content);
16395
+ console.log(source_default.green(`\u2713 Skill \u5B89\u88C5\u6210\u529F!`));
16396
+ console.log(` ID: ${source_default.cyan(repoName)}`);
16397
+ console.log(` \u540D\u79F0: ${name || repoName}`);
16398
+ if (shortDesc) console.log(` \u529F\u80FD: ${source_default.dim(shortDesc)}`);
16399
+ console.log(source_default.dim(` \u4F7F\u7528 /sk ${repoName} \u52A0\u8F7D`));
16400
+ break;
16401
+ }
16402
+ case "uninstall": {
16403
+ if (!arg) {
16404
+ console.log("\u7528\u6CD5: /uninstall <skill-id>");
16405
+ break;
16406
+ }
16407
+ const uninstallPath = (0, import_path5.join)(SKILLS_DIR, arg);
16408
+ if (!(0, import_fs5.existsSync)(uninstallPath)) {
16409
+ console.log(source_default.red(`\u672A\u627E\u5230\u5DF2\u5B89\u88C5\u7684 Skill: ${arg}`));
16410
+ console.log(source_default.dim("\u6CE8\u610F: \u53EA\u80FD\u5378\u8F7D\u901A\u8FC7 /install \u5B89\u88C5\u7684\u7B2C\u4E09\u65B9 Skill"));
16411
+ break;
16412
+ }
16413
+ const ans = await question(rl, source_default.yellow(` \u786E\u8BA4\u5378\u8F7D Skill "${arg}"\uFF1F[y/N] \u203A `));
16414
+ if (ans.trim().toLowerCase() !== "y") {
16415
+ console.log(source_default.dim(" \u5DF2\u53D6\u6D88"));
16416
+ break;
16417
+ }
16418
+ const rmResult = await executeTool("bash", { command: `rm -rf "${uninstallPath}"` });
16419
+ if (rmResult.error) {
16420
+ console.log(source_default.red(`\u5378\u8F7D\u5931\u8D25: ${rmResult.error}`));
16421
+ } else {
16422
+ injectedSkills.delete(arg);
16423
+ console.log(source_default.green(`\u2713 Skill "${arg}" \u5DF2\u5378\u8F7D`));
16424
+ }
16425
+ break;
16426
+ }
15139
16427
  case "compact": {
15140
- const tokens = estimateTokens(history);
16428
+ const tokens = estimateTokens2(history);
15141
16429
  if (history.length < 4) {
15142
16430
  console.log(source_default.dim("\u5BF9\u8BDD\u592A\u77ED\uFF0C\u65E0\u9700\u538B\u7F29"));
15143
16431
  break;
@@ -15159,7 +16447,7 @@ ${summary}` },
15159
16447
  { role: "assistant", content: "\u2713 \u5DF2\u7406\u89E3\u4E4B\u524D\u7684\u5BF9\u8BDD\u6458\u8981\uFF0C\u8BF7\u7EE7\u7EED\u3002" },
15160
16448
  ...recent
15161
16449
  ];
15162
- const after = estimateTokens(newHistory);
16450
+ const after = estimateTokens2(newHistory);
15163
16451
  console.log(source_default.green(`\u2713 \u538B\u7F29\u5B8C\u6210: ${history.length} \u6761\u6D88\u606F \u2192 ${newHistory.length} \u6761\uFF0C~${Math.round(after / 1e3)}k tokens`));
15164
16452
  return { injectHistory: newHistory };
15165
16453
  } catch (err) {
@@ -15167,6 +16455,52 @@ ${summary}` },
15167
16455
  }
15168
16456
  break;
15169
16457
  }
16458
+ case "skills": {
16459
+ if (injectedSkills.size === 0) {
16460
+ console.log(source_default.dim("\u5F53\u524D\u4F1A\u8BDD\u672A\u52A0\u8F7D\u4EFB\u4F55 Skill"));
16461
+ console.log(source_default.dim("\u4F7F\u7528 /sk <\u5173\u952E\u8BCD> \u52A0\u8F7D\uFF0C\u6216\u76F4\u63A5\u63CF\u8FF0\u4EFB\u52A1\u81EA\u52A8\u6FC0\u6D3B"));
16462
+ } else {
16463
+ console.log(source_default.bold(`
16464
+ \u5F53\u524D\u5DF2\u52A0\u8F7D\u7684 Skills (${injectedSkills.size} \u4E2A):
16465
+ `));
16466
+ for (const [id, name] of injectedSkills) {
16467
+ console.log(` ${source_default.green("\u25CF")} ${source_default.cyan(id)} ${source_default.dim("\u2014 " + name)}`);
16468
+ console.log(source_default.dim(` /unload ${id} \u53EF\u5378\u8F7D\u6B64 Skill`));
16469
+ }
16470
+ console.log();
16471
+ console.log(source_default.dim("\u63D0\u793A: /clear \u6E05\u7A7A\u5168\u90E8\u5BF9\u8BDD\u548C Skills | /unload <id> \u5378\u8F7D\u5355\u4E2A"));
16472
+ }
16473
+ break;
16474
+ }
16475
+ case "unload": {
16476
+ if (!arg) {
16477
+ console.log("\u7528\u6CD5: /unload <skill-id>");
16478
+ console.log(source_default.dim("\u4F7F\u7528 /skills \u67E5\u770B\u5F53\u524D\u5DF2\u52A0\u8F7D\u7684 Skills"));
16479
+ break;
16480
+ }
16481
+ const loadedIds = Array.from(injectedSkills.keys());
16482
+ const targetId = loadedIds.find((id) => id === arg || id.includes(arg));
16483
+ if (!targetId) {
16484
+ console.log(source_default.yellow(`\u672A\u627E\u5230\u5DF2\u52A0\u8F7D\u7684 Skill: "${arg}"`));
16485
+ console.log(source_default.dim("\u4F7F\u7528 /skills \u67E5\u770B\u5F53\u524D\u5DF2\u52A0\u8F7D\u7684 Skills"));
16486
+ break;
16487
+ }
16488
+ const SKILL_MARKER = `[Skill \u5DF2\u52A0\u8F7D: ${targetId}]`;
16489
+ let removed = 0;
16490
+ for (let i2 = history.length - 1; i2 >= 0; i2--) {
16491
+ const content = typeof history[i2].content === "string" ? history[i2].content : "";
16492
+ if (history[i2].role === "user" && content.startsWith(SKILL_MARKER)) {
16493
+ const toRemove = i2 + 1 < history.length && history[i2 + 1].role === "assistant" ? 2 : 1;
16494
+ history.splice(i2, toRemove);
16495
+ removed += toRemove;
16496
+ break;
16497
+ }
16498
+ }
16499
+ injectedSkills.delete(targetId);
16500
+ console.log(source_default.green(`\u2713 Skill "${targetId}" \u5DF2\u5378\u8F7D`));
16501
+ if (removed > 0) console.log(source_default.dim(` \u5DF2\u4ECE\u5BF9\u8BDD\u5386\u53F2\u4E2D\u79FB\u9664 ${removed} \u6761\u6CE8\u5165\u6D88\u606F`));
16502
+ break;
16503
+ }
15170
16504
  case "think": {
15171
16505
  const val = arg.toLowerCase();
15172
16506
  if (val === "on" || val === "1" || val === "true") {
@@ -15191,29 +16525,41 @@ ${summary}` },
15191
16525
  const allSkills = collectAllSkills();
15192
16526
  const idMatch = allSkills.find((e2) => e2.id === arg || e2.id.startsWith(arg) || e2.id.includes(arg));
15193
16527
  if (idMatch) {
15194
- injectSkill(arg, history);
16528
+ await injectSkill(idMatch.id, history, injectedSkills, rl);
15195
16529
  break;
15196
16530
  }
15197
- const { routes, topScore } = routeSkill(arg);
15198
- if (routes.length === 0 || topScore < 4) {
16531
+ const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
16532
+ let si = 0;
16533
+ const spinTimer = setInterval(() => {
16534
+ process.stdout.write(`\r ${source_default.cyan(spinner[si++ % spinner.length])} \u6B63\u5728\u7528 AI \u641C\u7D22\u6700\u5339\u914D\u7684 Skills...`);
16535
+ }, 80);
16536
+ const llmRecs = await llmRecommendSkills(arg);
16537
+ clearInterval(spinTimer);
16538
+ process.stdout.write("\r" + " ".repeat(50) + "\r");
16539
+ if (llmRecs.length === 0) {
16540
+ console.log(source_default.yellow(" AI \u63A8\u8350\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u663E\u793A\u5173\u952E\u8BCD\u5339\u914D\u7ED3\u679C:"));
15199
16541
  listSkills(arg);
15200
16542
  break;
15201
16543
  }
15202
- if (routes.length === 1 || topScore >= 10) {
15203
- console.log(source_default.dim(`\u6839\u636E\u63CF\u8FF0\u5339\u914D\u5230: ${routes[0].id}`));
15204
- injectSkill(routes[0].id, history);
15205
- } else {
15206
- console.log(source_default.bold(`
15207
- \u6839\u636E\u63CF\u8FF0\u63A8\u8350\u4EE5\u4E0B Skills:
16544
+ console.log(source_default.bold(`
16545
+ AI \u4E3A\u60A8\u63A8\u8350\u4EE5\u4E0B Skills (\u8F93\u5165\u5E8F\u53F7\u76F4\u63A5\u52A0\u8F7D):
15208
16546
  `));
15209
- routes.forEach((r2, i2) => {
15210
- const marker = i2 === 0 ? source_default.green("\u25B6") : source_default.dim(" ");
15211
- console.log(` ${marker} ${source_default.cyan(r2.id)} \u2014 ${r2.name}`);
15212
- if (i2 > 0) console.log(source_default.dim(` /sk ${r2.id} \u53EF\u6FC0\u6D3B\u6B64\u9879`));
15213
- });
15214
- console.log();
15215
- console.log(source_default.dim("\u5DF2\u81EA\u52A8\u6FC0\u6D3B\u7B2C\u4E00\u4E2A\uFF0C\u5982\u9700\u5207\u6362\u8BF7\u4F7F\u7528\u4E0A\u65B9\u547D\u4EE4"));
15216
- injectSkill(routes[0].id, history);
16547
+ llmRecs.forEach((r2, i2) => {
16548
+ const num = source_default.bold.cyan(`[${i2 + 1}]`);
16549
+ console.log(` ${num} ${source_default.cyan(r2.id)}`);
16550
+ console.log(` ${source_default.dim(r2.reason)}`);
16551
+ });
16552
+ console.log();
16553
+ console.log(source_default.dim(" \u8F93\u5165\u5E8F\u53F7 1-" + llmRecs.length + " \u52A0\u8F7D\uFF0C\u76F4\u63A5\u56DE\u8F66\u53D6\u6D88\uFF0C\u6216\u8F93\u5165 /sk <id> \u52A0\u8F7D\u5176\u4ED6"));
16554
+ console.log();
16555
+ const choice = await question(rl, source_default.blue(" \u9009\u62E9 \u203A "));
16556
+ const choiceNum = parseInt(choice.trim(), 10);
16557
+ if (!isNaN(choiceNum) && choiceNum >= 1 && choiceNum <= llmRecs.length) {
16558
+ await injectSkill(llmRecs[choiceNum - 1].id, history, injectedSkills, rl);
16559
+ } else if (choice.trim() === "") {
16560
+ console.log(source_default.dim(" \u5DF2\u53D6\u6D88"));
16561
+ } else {
16562
+ console.log(source_default.dim(` \u63D0\u793A: \u4F7F\u7528 /sk ${choice.trim()} \u52A0\u8F7D\u6307\u5B9A Skill`));
15217
16563
  }
15218
16564
  break;
15219
16565
  }
@@ -15249,8 +16595,8 @@ ${summary}` },
15249
16595
  console.log("\u7528\u6CD5: /cd <\u8DEF\u5F84>");
15250
16596
  break;
15251
16597
  }
15252
- const target = (0, import_path4.resolve)(arg.replace(/^~/, (0, import_os3.homedir)()));
15253
- if (!(0, import_fs4.existsSync)(target)) {
16598
+ const target = (0, import_path5.resolve)(arg.replace(/^~/, (0, import_os3.homedir)()));
16599
+ if (!(0, import_fs5.existsSync)(target)) {
15254
16600
  console.log(source_default.red(`\u8DEF\u5F84\u4E0D\u5B58\u5728: ${target}`));
15255
16601
  break;
15256
16602
  }
@@ -15263,11 +16609,12 @@ ${summary}` },
15263
16609
  break;
15264
16610
  case "tools":
15265
16611
  console.log(source_default.bold("AI \u53EF\u8C03\u7528\u7684\u5DE5\u5177:"));
15266
- console.log(` ${source_default.cyan("bash")} \u6267\u884C Shell \u547D\u4EE4 (R/Python/bash \u811A\u672C\u3001\u751F\u4FE1\u5DE5\u5177)`);
15267
- console.log(` ${source_default.cyan("read_file")} \u8BFB\u53D6\u6587\u4EF6\u5185\u5BB9 (\u652F\u6301 ~/ \u8DEF\u5F84)`);
16612
+ console.log(` ${source_default.cyan("bash")} \u6267\u884C Shell \u547D\u4EE4 (R/Python/bash \u811A\u672C\u3001\u751F\u4FE1\u5DE5\u5177) \u2014 \u5B9E\u65F6\u6D41\u5F0F\u8F93\u51FA`);
16613
+ console.log(` ${source_default.cyan("read_file")} \u8BFB\u53D6\u6587\u4EF6\u5185\u5BB9 (\u652F\u6301 ~/ \u8DEF\u5F84\uFF0C\u9ED8\u8BA4 500 \u884C)`);
15268
16614
  console.log(` ${source_default.cyan("write_file")} \u521B\u5EFA\u6216\u8986\u5199\u6587\u4EF6`);
15269
16615
  console.log(` ${source_default.cyan("list_dir")} \u5217\u51FA\u76EE\u5F55\u5185\u5BB9`);
15270
16616
  console.log(` ${source_default.cyan("search_files")} glob \u641C\u7D22\u6587\u4EF6 (\u5982 *.R, *.csv)`);
16617
+ console.log(` ${source_default.cyan("fetch_geo")} \u67E5\u8BE2 NCBI GEO \u6570\u636E\u5E93 (GSE/GDS/GPL/GSM \u7F16\u53F7)`);
15271
16618
  console.log();
15272
16619
  console.log(source_default.dim("\u63D0\u793A: \u76F4\u63A5\u63CF\u8FF0\u4EFB\u52A1\uFF0CAI \u4F1A\u81EA\u52A8\u51B3\u5B9A\u8C03\u7528\u54EA\u4E2A\u5DE5\u5177"));
15273
16620
  break;
@@ -15302,13 +16649,39 @@ async function main() {
15302
16649
  console.log(` ${source_default.bold("\u6A21\u578B:")} ${source_default.green(cfg.model)}`);
15303
16650
  console.log(` ${source_default.bold("Skills:")} ${totalSkills > 0 ? source_default.green(`${totalSkills} \u4E2A`) : source_default.yellow("\u672A\u5B89\u88C5")} ${source_default.dim("(/sk \u641C\u7D22 /cat \u5206\u7C7B\u76EE\u5F55)")}`);
15304
16651
  console.log(` ${source_default.bold("\u5DE5\u5177:")} bash \xB7 read_file \xB7 write_file \xB7 list_dir \xB7 search_files`);
16652
+ console.log(` ${source_default.bold("\u65B0\u529F\u80FD:")} /sessions /resume /checkpoint /run /check-env /install /diff`);
15305
16653
  console.log(source_default.bold.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
16654
+ const lastSess = getLastSession();
16655
+ if (lastSess) {
16656
+ const age = (() => {
16657
+ const diff = Date.now() - new Date(lastSess.updatedAt).getTime();
16658
+ const h2 = Math.floor(diff / 36e5);
16659
+ const d2 = Math.floor(diff / 864e5);
16660
+ return d2 > 0 ? `${d2}\u5929\u524D` : h2 > 0 ? `${h2}\u5C0F\u65F6\u524D` : "\u521A\u521A";
16661
+ })();
16662
+ console.log(source_default.dim(` \u4E0A\u6B21\u4F1A\u8BDD: ${lastSess.id} ${age} /resume \u6062\u590D`));
16663
+ }
15306
16664
  console.log(source_default.dim(" \u8F93\u5165\u95EE\u9898\u5F00\u59CB\u5BF9\u8BDD /help \u67E5\u770B\u547D\u4EE4 /cat \u6280\u80FD\u5206\u7C7B @\u6587\u4EF6\u8DEF\u5F84 \u5185\u5D4C\u6587\u4EF6"));
15307
16665
  console.log();
15308
16666
  const systemPrompt = buildSystemPrompt();
15309
16667
  let history = [];
15310
16668
  let thinkMode = false;
15311
- const injectedSkills = /* @__PURE__ */ new Set();
16669
+ const injectedSkills = /* @__PURE__ */ new Map();
16670
+ const sessionId = newSessionId();
16671
+ const sessionCreatedAt = (/* @__PURE__ */ new Date()).toISOString();
16672
+ SESSION_CTX.id = sessionId;
16673
+ SESSION_CTX.createdAt = sessionCreatedAt;
16674
+ const wdirSnapshot = snapshotWorkdir(process.cwd());
16675
+ SESSION_CTX.wdirSnapshot = wdirSnapshot;
16676
+ function autoSaveSession() {
16677
+ if (history.length === 0) return;
16678
+ try {
16679
+ saveSession(sessionId, sessionCreatedAt, history, Array.from(injectedSkills.keys()), sessionCreatedAt);
16680
+ } catch {
16681
+ }
16682
+ }
16683
+ let lastCheckpointMsgCount = 0;
16684
+ const CHECKPOINT_INTERVAL = 6;
15312
16685
  while (true) {
15313
16686
  let input;
15314
16687
  const thinkIndicator = thinkMode ? source_default.yellow("[\u601D\u8003]") + " " : "";
@@ -15325,7 +16698,7 @@ async function main() {
15325
16698
  break;
15326
16699
  }
15327
16700
  if (trimmed.startsWith("/")) {
15328
- const result = await handleCommand(trimmed, rl, history, thinkMode);
16701
+ const result = await handleCommand(trimmed, rl, history, thinkMode, injectedSkills);
15329
16702
  if (result.exit) break;
15330
16703
  if (result.clearHistory) {
15331
16704
  history = [];
@@ -15335,13 +16708,34 @@ async function main() {
15335
16708
  if (result.thinkMode !== void 0) thinkMode = result.thinkMode;
15336
16709
  continue;
15337
16710
  }
16711
+ if (trimmed.startsWith("!")) {
16712
+ const cmd = trimmed.slice(1).trim();
16713
+ if (!cmd) {
16714
+ console.log(source_default.yellow("\u7528\u6CD5: !<\u547D\u4EE4> \u4F8B: !ls -la !Rscript analysis.R !python script.py"));
16715
+ continue;
16716
+ }
16717
+ console.log(source_default.dim(`[\u76F4\u63A5\u6267\u884C] ${cmd}`));
16718
+ const t0 = Date.now();
16719
+ const result = await executeTool("bash", { command: cmd }, (chunk) => {
16720
+ process.stdout.write(source_default.dim(" \u2502 ") + chunk);
16721
+ });
16722
+ const elapsed = ((Date.now() - t0) / 1e3).toFixed(1);
16723
+ if (result.error) {
16724
+ console.log(source_default.yellow(`
16725
+ \u2717 ${result.error} ${source_default.dim("(" + elapsed + "s)")}`));
16726
+ } else {
16727
+ console.log(source_default.green(`
16728
+ \u2713 \u5B8C\u6210 ${source_default.dim("(" + elapsed + "s)")}`));
16729
+ }
16730
+ console.log();
16731
+ continue;
16732
+ }
15338
16733
  const { routes: suggestedRoutes, topScore } = routeSkill(trimmed);
15339
16734
  const newRoutes = suggestedRoutes.filter((r2) => !injectedSkills.has(r2.id));
15340
16735
  if (newRoutes.length === 1 && topScore >= 8) {
15341
16736
  const r2 = newRoutes[0];
15342
- const ok = injectSkill(r2.id, history);
16737
+ const ok = await injectSkill(r2.id, history, injectedSkills, rl, true);
15343
16738
  if (ok) {
15344
- injectedSkills.add(r2.id);
15345
16739
  console.log(source_default.dim(" (\u63D0\u793A: /clear \u53EF\u6E05\u9664\u4E0A\u4E0B\u6587\u540E\u5207\u6362 Skill)"));
15346
16740
  }
15347
16741
  } else if (newRoutes.length >= 2 && topScore >= 4) {
@@ -15360,6 +16754,17 @@ ${expanded}` : expanded;
15360
16754
  const reply = await chat(history, currentCfg, systemPrompt);
15361
16755
  history.push({ role: "assistant", content: reply });
15362
16756
  history = await maybeCompact(history, currentCfg);
16757
+ autoSaveSession();
16758
+ if (history.length - lastCheckpointMsgCount >= CHECKPOINT_INTERVAL) {
16759
+ const label = `\u7B2C ${Math.floor(history.length / 2)} \u8F6E`;
16760
+ saveCheckpoint(sessionId, label, history, Array.from(injectedSkills.keys()));
16761
+ lastCheckpointMsgCount = history.length;
16762
+ }
16763
+ if (injectedSkills.size > 0) {
16764
+ const ids = Array.from(injectedSkills.keys()).join(source_default.dim(" \xB7 "));
16765
+ console.log(source_default.dim(`
16766
+ [\u6FC0\u6D3B Skill: ${ids}]`));
16767
+ }
15363
16768
  } catch (err) {
15364
16769
  const msg = err instanceof Error ? err.message : String(err);
15365
16770
  console.error(source_default.red(`