Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions src/qoder-language-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,44 @@ function installSdkLogFilter(): void {
// 模块加载即生效
installSdkLogFilter()

// ── 将纯文本 prompt 包装为 streaming 模式 ──────────────────────────────────────
// 避免 --print 参数过长导致 Windows ENAMETOOLONG(命令行限制 ~32767 字符)
function wrapStringAsStream(prompt: string, sessionId?: string): AsyncIterable<{ type: string; session_id: string; message: { role: string; content: Array<{ type: string; text: string }> }; parent_tool_use_id: null }> {
async function* toStream() {
yield {
type: 'user',
session_id: sessionId ?? 'default',
message: { role: 'user', content: [{ type: 'text', text: prompt }] },
parent_tool_use_id: null as const,
}
}
return toStream()
}

// ── qodercli 二进制路径解析 ───────────────────────────────────────────────────

function resolveQoderCLI(): string | undefined {
const isWin = process.platform === 'win32'
// Windows 上优先搜 .exe,再 .cmd;Unix 搜无后缀
const candidates = isWin ? ['qodercli.exe', 'qodercli.cmd', 'qodercli'] : ['qodercli']
// 1. 全局 PATH 里的 qodercli(用户自行安装,优先)
const pathDirs = (process.env.PATH ?? '').split(path.delimiter)
for (const dir of pathDirs) {
const p = path.join(dir, 'qodercli')
if (fs.existsSync(p)) return p
for (const name of candidates) {
for (const dir of pathDirs) {
const p = path.join(dir, name)
if (fs.existsSync(p)) return p
}
}

// 2. SDK 默认本地安装路径:~/.qoder/local/qodercli
// 2. npm 全局包内嵌的真实二进制(Windows 特有)
if (isWin) {
const npmPrefix = path.join(os.homedir(), 'AppData', 'Roaming', 'npm')
const embedded = path.join(npmPrefix, 'node_modules', '@qoder-ai', 'qodercli', 'bin', 'qodercli.exe')
if (fs.existsSync(embedded)) return embedded
}
// 3. SDK 默认本地安装路径:~/.qoder/local/qodercli
const localCli = path.join(os.homedir(), '.qoder', 'local', 'qodercli')
if (fs.existsSync(localCli)) return localCli

// 3. 回退:~/.qoder/bin/qodercli/qodercli-<version>(取最新版本)
// 4. 回退:~/.qoder/bin/qodercli/qodercli-<version>(取最新版本)
const binDir = path.join(os.homedir(), '.qoder', 'bin', 'qodercli')
if (fs.existsSync(binDir)) {
try {
Expand Down Expand Up @@ -603,8 +626,9 @@ export class QoderLanguageModel implements LanguageModelV2 {
let suppressFurtherAssistantContent = false

try {
// query() 是单次查询的最优路径(QoderAgentSDKClient 是双向交互会话,每次 connect() 冷启动更慢)
const qoderQuery = query({ prompt, options: { ...qoderOptions, abortController } })
// 强制 streaming 模式:纯文本 prompt 通过 stdin 传递,避免 --print 参数过长导致 Windows ENAMETOOLONG
const streamPrompt = typeof prompt === 'string' ? wrapStringAsStream(prompt, qoderOptions.sessionId) : prompt
const qoderQuery = query({ prompt: streamPrompt, options: { ...qoderOptions, abortController } })
qoderQueryRef = qoderQuery
let sdkMsgCount = 0
for await (const msg of qoderQuery) {
Expand Down
47 changes: 12 additions & 35 deletions src/vendor/qoder-agent-sdk.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -560,16 +560,9 @@ Or provide the path via options:
* Build CLI command with arguments
*/
buildCommand() {
const cmd = [this.cliPath, "--output-format", "stream-json", "--verbose"];
const cmd = [this.cliPath, "--output-format", "stream-json"];
const config = getConfig();
const storageDir = this.options.storageDir ?? config.storageDir;
if (storageDir) {
cmd.push("--storage-dir", storageDir);
}
const resourceDir = this.options.resourceDir ?? config.resourceDir;
if (resourceDir) {
cmd.push("--resource-dir", resourceDir);
}
// --storage-dir, --resource-dir, --verbose not supported by qodercli; removed for compatibility
if (this.options.tools !== void 0) {
if (Array.isArray(this.options.tools)) {
if (this.options.tools.length === 0) {
Expand All @@ -584,24 +577,12 @@ Or provide the path via options:
if (this.options.allowedTools && this.options.allowedTools.length > 0) {
cmd.push("--allowed-tools", this.options.allowedTools.join(","));
}
if (this.options.maxTurns !== void 0) {
cmd.push("--max-turns", String(this.options.maxTurns));
}
if (this.options.maxBudgetUsd !== void 0) {
cmd.push("--max-budget-usd", String(this.options.maxBudgetUsd));
}
if (this.options.disallowedTools && this.options.disallowedTools.length > 0) {
cmd.push("--disallowed-tools", this.options.disallowedTools.join(","));
}
if (this.options.model) {
cmd.push("--model", this.options.model);
}
if (this.options.fallbackModel) {
cmd.push("--fallback-model", this.options.fallbackModel);
}
if (this.options.betas && this.options.betas.length > 0) {
cmd.push("--betas", this.options.betas.join(","));
}
if (this.options.permissionMode && this.options.permissionMode === "bypassPermissions") {
cmd.push("--yolo");
}
Expand All @@ -611,9 +592,6 @@ Or provide the path via options:
if (this.options.resume) {
cmd.push("--resume", this.options.resume);
}
if (this.options.resumeSessionAt) {
cmd.push("--resume-session-at", this.options.resumeSessionAt);
}
if (this.options.sessionId && !this.options.resume) {
cmd.push("--session-id", this.options.sessionId);
}
Expand All @@ -640,11 +618,16 @@ Or provide the path via options:
}
}
if (Object.keys(serversForCli).length > 0) {
cmd.push("--mcp-config", JSON.stringify({ mcpServers: serversForCli }));
}
}
if (this.options.includePartialMessages) {
cmd.push("--include-partial-messages");
const mcpJson = JSON.stringify({ mcpServers: serversForCli });
// Windows 命令行限制 ~32767 字符,超长时写入临时文件
if (mcpJson.length > 8000) {
const tmpFile = path2.join(os.tmpdir(), `qoder-mcp-${Date.now()}.json`);
fs.writeFileSync(tmpFile, mcpJson, 'utf-8');
this.tempFiles.push(tmpFile);
cmd.push("--mcp-config", tmpFile);
} else {
cmd.push("--mcp-config", mcpJson);
}
}
if (this.options.forkSession) {
cmd.push("--fork-session");
Expand Down Expand Up @@ -682,12 +665,6 @@ Or provide the path via options:
}
}
}
if (this.options.maxThinkingTokens !== void 0) {
cmd.push("--max-thinking-tokens", String(this.options.maxThinkingTokens));
}
if (this.options.outputFormat && this.options.outputFormat.type === "json_schema") {
cmd.push("--json-schema", JSON.stringify(this.options.outputFormat.schema));
}
if (this.usePreparedNonStreaming) {
for (const attachment of this.preparedAttachments) {
cmd.push("--attachment", attachment);
Expand Down