Skip to content
Merged
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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ CharacterArc(弧光)不是"只会对话的 AI 壳子",而是一套围绕
<tr>
<td colspan="2" valign="top">
<h4>🌐 多厂商接入</h4>
<p>支持所有 OpenAI 兼容接口(DeepSeek、通义千问、智谱、Kimi、SiliconFlow、Ollama 等)和 Anthropic 协议(官方及中转站),只需选协议填地址即可。</p>
<p>支持 Codex CLI、OpenAI 兼容接口(DeepSeek、通义千问、智谱、Kimi、SiliconFlow、Ollama 等)和 Anthropic 协议(官方及中转站)。</p>
</td>
</tr>
</table>
Expand Down Expand Up @@ -184,7 +184,7 @@ CharacterArc(弧光)不是"只会对话的 AI 壳子",而是一套围绕
| 富文本编辑 | TipTap |
| 持久化 | SQLite(主进程) |
| 关系图谱 | Cytoscape |
| AI SDK | Vercel AI SDK (@ai-sdk/openai, @ai-sdk/anthropic) |
| AI 接入 | Vercel AI SDK(OpenAI / Anthropic)+ 本机 Codex CLI |
| 文档解析 | mammoth (.docx)、marked (Markdown) |

## 📋 环境要求
Expand Down Expand Up @@ -234,6 +234,7 @@ macOS 产物为 ad-hoc 签名、未公证的 `.dmg` / `.zip`,适合内部安
<b>📝 文本生成</b>
<ul>
<li>支持维护多套接口配置,并在标题栏快速切换</li>
<li>Codex CLI:复用本机登录状态,无需 API Key;支持自动查找路径、拉取模型和设置推理强度</li>
<li>协议类型:OpenAI 兼容协议 / Anthropic 协议</li>
<li>Base URL(只需填域名或路径前缀,系统自动补全 /v1)</li>
<li>API Key</li>
Expand Down
22 changes: 22 additions & 0 deletions electron/main/ai/agent/run-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { Tool, ToolContext } from './tools/types'
import { stripReasoningMarkup } from '../reasoning'
import { isAiStreamIdleTimeoutError } from '../sse'
import { resolveSamplingOptions } from '../request-options'
import { isCodexCliProvider } from '@shared/ai-provider-catalog'
import { runCodexCli } from '../codex-cli'

export type RunAgentParams = {
settings: AppSettings
Expand Down Expand Up @@ -94,6 +96,26 @@ function mergeUsage(a: AiRunUsage | undefined, b: AiRunUsage | undefined): AiRun

export async function runAgent(params: RunAgentParams): Promise<RunAgentResult> {
const maxSteps = params.maxSteps ?? 8
if (isCodexCliProvider(params.settings.provider)) {
params.handlers.onAgentStatus('正在通过 Codex CLI 思考...', 1, 1)
const result = await runCodexCli(
params.settings,
{
system: [
params.systemPrompt,
'当前通过 Codex CLI 文本模式运行,CharacterArc 的进程内工具不可用。请直接根据已提供的上下文回答,不要尝试调用工具。'
].join('\n\n'),
user: params.userPrompt
},
{ signal: params.ctx.signal, handlers: params.handlers }
)
return {
finalText: stripReasoningMarkup(result.text),
toolCalls: [],
iterations: 1,
usage: result.usage
}
}
const toolCalls: ToolCallTrace[] = []
const toolStartTimes = new Map<string, number>()
let stepCount = 0
Expand Down
81 changes: 81 additions & 0 deletions electron/main/ai/codex-cli.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import assert from 'node:assert/strict'
import test from 'node:test'

import { buildCodexExecArgs, parseCodexJsonLine, resolveCodexCommand } from './codex-cli.ts'
import { normalizeSettings, validateSettings } from './settings.ts'
import { getAiProviderCatalogEntry, shouldTryStreamingAgent } from '../../shared/ai-provider-catalog.ts'

const codexSettings = {
provider: 'codex-cli',
model: '',
apiKey: '',
baseUrl: '',
codexCliPath: '',
codexReasoningEffort: 'default',
embeddingModel: '',
imageModel: '',
imageApiKey: '',
imageBaseUrl: ''
}

test('Codex CLI 预设不要求 API 地址或 Key,并使用 default 模型', () => {
const preset = getAiProviderCatalogEntry('codex-cli')
const normalized = normalizeSettings(codexSettings)

assert.equal(preset?.transport, 'codex-cli')
assert.equal(preset?.supportsEmbedding, false)
assert.equal(normalized.model, 'default')
assert.equal(normalized.baseUrl, '')
assert.doesNotThrow(() => validateSettings(normalized))
assert.equal(shouldTryStreamingAgent('global-assistant', 'codex-cli', 'default'), false)
})

test('Codex exec 使用只读 JSONL 与 stdin,并传递模型和推理强度', () => {
const args = buildCodexExecArgs({
...codexSettings,
model: 'gpt-5.6-sol',
codexReasoningEffort: 'high'
})

assert.deepEqual(args.slice(0, 6), [
'exec',
'--json',
'--skip-git-repo-check',
'--sandbox',
'read-only',
'-c'
])
assert.ok(args.includes('features.shell_tool=false'))
assert.ok(args.includes('model_reasoning_effort="high"'))
assert.deepEqual(args.slice(-3), ['--model', 'gpt-5.6-sol', '-'])
})

test('Codex CLI 路径拒绝混入 shell 环境变量', () => {
assert.throws(
() => resolveCodexCommand({
...codexSettings,
codexCliPath: 'HTTPS_PROXY=http://127.0.0.1:7890 /opt/homebrew/bin/codex'
}),
/只能填写可执行文件路径/
)
})

test('Codex JSONL 事件转换为正文、推理和 token 用量', () => {
const reasoning = []
assert.deepEqual(
parseCodexJsonLine(
'{"type":"item.completed","item":{"type":"reasoning","text":"分析中"}}',
{ onTextDelta() {}, onReasoningDelta: (delta) => reasoning.push(delta) }
),
{}
)
assert.deepEqual(reasoning, ['分析中'])
assert.deepEqual(
parseCodexJsonLine('{"type":"item.completed","item":{"type":"agent_message","text":"完成"}}'),
{ text: '完成' }
)
assert.deepEqual(
parseCodexJsonLine('{"type":"turn.completed","usage":{"input_tokens":12,"output_tokens":8}}'),
{ usage: { promptTokens: 12, completionTokens: 8, totalTokens: 20 } }
)
})
Loading