npm package: @mmteam/jsllm
Description: MM Team LLM library (providers + chat/session runner)
Latest version: 0.1.16

# @mmteam/jsllm

独立的 LLM 能力库：封装多供应商差异（OpenAI 兼容、Gemini）与会话执行（含工具循环/落盘）。

发布形态：
- 单文件运行时代码：`dist/index.cjs`（CommonJS，已 bundle 所有运行时依赖）
- 单文件类型声明：`dist/types.d.ts`
- 发布包零 dependencies（下游安装后无需额外安装 OpenAI/Gemini SDK 等依赖）

## 功能概览
- 统一供应商：OpenAI 兼容 / Gemini
- 统一会话：会话目录管理、消息落盘、历史恢复、usage 累计
- 流式输出：stdout 输出回答；stderr 输出诊断与 think/reasoning（若模型提供）
- 工具循环：内置 `shell` 工具（模型触发时在工作区执行命令，并回填输出）

## 安装

```bash
npm i @mmteam/jsllm
```

## 环境要求
- Node.js >= 20

## 快速开始

### CommonJS（推荐）
```js
const path = require("node:path");
const { chatInSession } = require("@mmteam/jsllm");

async function main() {
  await chatInSession({
    init: {
      providerName: "openai",
      apiType: "openai",
      apiKey: process.env.OPENAI_API_KEY,
      model: "gpt-4o-mini",
      temperature: 0.7,
    },
    workspaceRoot: process.cwd(),
    sessionRootDir: path.join(process.cwd(), ".magicai/chat-sessions"),
    messageArg: "hello",
  });
}

main().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});
```

输入规则：
- 若提供 `messageArg`，使用其作为用户输入
- 否则当 stdin 非 TTY 时读取 stdin；两者都没有则报错

### ESM 项目中使用（可选）
本包对外导出为 CommonJS（`require`）。在纯 ESM 项目中可用 `createRequire` 引用：
```js
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const { chatDirectText } = require("@mmteam/jsllm");
```

## API
对外仅导出 2 个函数（以及必要类型）：

详细参数、类型与中文说明：请参考 `node_modules/@mmteam/jsllm/dist/types.d.ts`（本仓库构建产物对应 `projects/jsllm/codex-zf/dist/types.d.ts`）。

### 如何选择？
| 能力 | `chatDirectText` | `chatInSession` |
|---|---|---|
| 目标 | 单轮生成（返回文本） | 会话引擎（落盘 + 工具循环 + 流式输出） |
| 输入 | 调用方提供 `messages` | `messageArg` 或 stdin；并自动加载会话历史 |
| 输出 | `Promise<string>` | stdout 流式输出；会话文件落盘；stderr 输出诊断/think |
| 工具 | 不启用、不执行 | 内置 `shell`（模型触发时执行并回填） |
| 适用场景 | 摘要/改写/分类等一次性生成；你自管存储 | CLI/交互式场景；需要可追溯、可续写会话 |

### `chatDirectText(init, messages): Promise<string>`
- 用途：不落盘、不启用工具的“单轮文本生成”，返回最终聚合文本
- 示例：
```js
const { chatDirectText } = require("@mmteam/jsllm");
const text = await chatDirectText(
  {
    providerName: "gemini",
    apiType: "gemini",
    apiKey: process.env.GEMINI_API_KEY,
    model: "gemini-1.5-pro",
  },
  [
    { role: "user", content: "Summarize: ..." },
  ],
);
console.log(text);
```

### `chatInSession(options): Promise<void>`
- 用途：执行一次“CLI 式会话主循环”（会话落盘 + 流式输出 + 工具循环）
- 重要参数：
  - `workspaceRoot`：工作区根目录（用于解析 `public/agents`，以及工具执行 cwd）
  - `sessionRootDir`：会话根目录（会在其下创建会话目录与 `latest` 软链）
  - `systemPrompt`：支持内联文本或 `@` 引用文件/agent 名称（见下方说明）
  - `consoleOutput`：是否写入 stdout/stderr（默认 `true`）
  - `onEvent`：实时输出回调（区分正常回复 text 与 think，并携带与落盘一致的文件名/路径）

## 供应商与配置

### OpenAI 兼容（`apiType: "openai"`）
```js
init: {
  providerName: "openai", // 或 dashscope / 自建网关等；qwen* 见下
  apiType: "openai",
  apiKey: "...",
  baseUrl: "https://your-gateway.example.com", // 可选
  model: "gpt-4o-mini",
  temperature: 0.7,
}
```

### Gemini（`apiType: "gemini"`）
```js
init: {
  providerName: "gemini",
  apiType: "gemini",
  apiKey: "...",
  baseUrl: "https://generativelanguage.googleapis.com", // 可选；仅代理/国内环境需要
  apiVersion: "v1beta", // 建议显式配置；预览模型推荐 v1beta
  model: "gemini-1.5-pro",
}
```
- `baseUrl`：非必须；未配置时使用 SDK 默认地址
- `apiVersion`：建议显式配置；预览模型常用 `v1beta`

### qwen*（OpenAI 兼容扩展）
判断规则：`providerName` 以 `qwen` 前缀（不区分大小写）即视为 qwen 模式（如 `qwen1`、`qwen-local`）。

thinking 参数：
- 推荐（统一入口）：`thinking.enabled`、`thinking.budgetTokens`
- 兼容（不推荐）：`qwen.enable_thinking`、`qwen.thinking_budget`（已废弃但仍支持；若同时存在则覆盖 `thinking`）

示例：
```js
init: {
  providerName: "qwen-local",
  apiType: "openai",
  apiKey: "...",
  baseUrl: "http://localhost:8000/v1",
  model: "qwen2.5",
  thinking: { enabled: true, budgetTokens: 2048 },
}
```

## 会话落盘说明（`chatInSession`）
- 会话根目录：由 `sessionRootDir` 指定
- 会话目录：`YYYYMMDD_HHMMSS/`
- `latest`：会话根目录下的软链，指向最近一次有效会话目录
- `usage.json`：会话累计用量（当供应商返回 usage 时会自动累加）
- 消息文件：按序号落盘，支持恢复历史继续会话
- `.think`：当模型输出 reasoning/think 时，会额外落盘同名 `.think` 文件

## 实时回调（`chatInSession.onEvent`）
你可以通过 `onEvent` 在应用侧接收流式输出，并区分正常回复与 think（事件中携带与落盘一致的文件名）：
```js
await chatInSession({
  /* ... */,
  consoleOutput: false,
  onEvent(e) {
    if (e.type === "assistant_text") {
      // e.textSoFar：当前轮累计的“正常回复”
    }
    if (e.type === "assistant_think") {
      // e.thinkSoFar：当前轮累计的 think/reasoning
    }
  },
});
```

## 多轮会话（`chatInSession`）
`chatInSession` 的多轮能力通过“会话目录落盘 + 下次加载历史”实现：每次调用会读取目标会话目录的历史消息并继续追加写入。

- 默认续写上一会话（推荐）：多次调用时保持相同的 `sessionRootDir`，且不传 `sessionArg`，库会自动复用 `latest` 指向的会话目录。
- 固定到指定会话：传入 `sessionArg: "my-session"`（相对 `sessionRootDir` 的相对子目录），每次调用都使用同一个值，即可稳定续写同一会话。
- systemPrompt 建议：同一会话通常只在“第一轮”设置 `systemPrompt`；后续轮次不再传（或改用 `sessionArg` 固定会话），避免触发创建新会话导致上下文断裂。

## systemPrompt / globalAgent（`chatInSession`）
- `systemPrompt` 支持两种形式：
  - 内联文本：直接传字符串
  - 引用文件/agent：以 `@` 开头，例如 `@公共智能体` 或 `@./path/to/prompt.txt`
- 当引用的是名称（非绝对路径且不以 `./` 开头）时，会在 `${workspaceRoot}/public/agents/` 下查找，并允许省略 `.agent.md` 扩展名

## 环境变量
- `MCHAT_REQUEST_TIMEOUT_MS`：OpenAI 兼容请求超时（毫秒）
- `MCHAT_DEBUG`：Gemini provider debug 输出（`true/1` 开启）

## 安全说明（工具）
`chatInSession` 内置 `shell` 工具：当模型触发工具调用时，会在 `workspaceRoot` 下通过 `bash -lc` 执行命令并回填输出。请仅在可信环境中使用，并确保运行账号权限可控。

## 开发
> 下述为仓库开发者使用；下游项目不需要执行 build。

```bash
npm run build
```