Files
claude-code/src/entrypoints/cli.tsx
claude-code-best 494eab7204 feat: 接入内建 weixin channel(同 #301 重构版本) (#303)
* feat: 接入 weixin 服务层与命令入口

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat: 注册内建 weixin channel 插件

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: 修正 channel permission relay 路由与能力判定

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: 修复 builtin channel 的 ChannelsNotice 误报

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* docs: 补充内建 weixin channel 使用说明

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* docs: 更新微信 channel 接入计划状态

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: 延迟加载 weixin 登录二维码依赖

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: 改用 qrcode 生成 weixin 登录二维码

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: 修正 vite 构建的 Windows 路径解析

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* chore: 删除临时规划文档 wx_channel.md 并还原 package.json 排序

wx_channel.md 内容已整合到 docs/features/channels.md,不再需要。
package.json 中 @ant/model-provider 位置从原始位置被无意移动,还原。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 将 weixin 模块从 src/ 迁移至 packages/weixin 工作区包

将 src/services/weixin/ 中的纯业务逻辑迁入 @claude-code-best/weixin
workspace 包,降低 src/ 耦合度。仅保留 server.ts 作为薄适配层。

- 迁移 7 个无修改的纯模块 (types/api/accounts/login/pairing/media/send)
- monitor.ts 内联 PERMISSION_REPLY_RE 正则,解除对 src/ 的依赖
- permissions.ts 本地定义 ChannelPermissionRequestParams 接口
- cli.ts 拆分:serve 子命令通过回调注入,login/access 保留在包内
- server.ts 重写为从 @claude-code-best/weixin 导入
- 新增 cli-serve.ts 作为 serve 入口薄壳

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修正 weixin barrel export 中 interface 的导出方式

ChannelPermissionRequestParams 是纯类型,必须用 export type 导出,
否则 Bun 运行时会报 "export not found" 错误。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 将 server.ts 迁入 packages/weixin,彻底移除 src/services/weixin/

通过依赖注入(WeixinServerDeps)解耦 src/ 依赖(analytics、config、
MCP channel schema),server.ts 完全移入包内。cli.tsx 入口处一次性
注入所有依赖。

src/services/weixin/ 目录已完全删除。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复 markdownToPlainText 中代码块正则的 ReDoS 风险

用非正则的线性扫描替代 \`\`\`[\s\S]*?\n([\s\S]*?)\`\`\` 匹配,
避免在含有大量重复 \`\`\` 序列的输入上触发多项式回溯。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: 1111 <11111@asd.c>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-19 21:33:27 +08:00

399 lines
15 KiB
TypeScript

#!/usr/bin/env bun
import { feature } from 'bun:bundle'
import { isEnvTruthy } from '../utils/envUtils.js'
// Runtime fallback for MACRO.* when not injected by build/dev defines.
// This happens when running cli.tsx directly (not via `bun run dev` or built dist/).
if (typeof globalThis.MACRO === 'undefined') {
;(globalThis as any).MACRO = {
VERSION: process.env.CLAUDE_CODE_VERSION || '2.1.888',
BUILD_TIME: new Date().toISOString(),
FEEDBACK_CHANNEL: '',
ISSUES_EXPLAINER: '',
NATIVE_PACKAGE_URL: '',
PACKAGE_URL: '',
VERSION_CHANGELOG: '',
}
}
if (isEnvTruthy(process.env.CLAUDE_CODE_FORCE_INTERACTIVE)) {
for (const stream of [process.stdin, process.stdout, process.stderr]) {
if (!stream.isTTY) {
try {
Object.defineProperty(stream, 'isTTY', {
value: true,
configurable: true,
})
} catch {
// Best-effort dev-only override for nested bun launch on Windows.
}
}
}
}
// Bugfix for corepack auto-pinning, which adds yarnpkg to peoples' package.jsons
// eslint-disable-next-line custom-rules/no-top-level-side-effects
process.env.COREPACK_ENABLE_AUTO_PIN = '0'
// Set max heap size for child processes in CCR environments (containers have 16GB)
// eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level, custom-rules/safe-env-boolean-check
if (process.env.CLAUDE_CODE_REMOTE === 'true') {
// eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level
const existing = process.env.NODE_OPTIONS || ''
// eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level
process.env.NODE_OPTIONS = existing
? `${existing} --max-old-space-size=8192`
: '--max-old-space-size=8192'
}
// Harness-science L0 ablation baseline. Inlined here (not init.ts) because
// BashTool/AgentTool/PowerShellTool capture DISABLE_BACKGROUND_TASKS into
// module-level consts at import time — init() runs too late. feature() gate
// DCEs this entire block from external builds.
// eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level
if (feature('ABLATION_BASELINE') && process.env.CLAUDE_CODE_ABLATION_BASELINE) {
for (const k of [
'CLAUDE_CODE_SIMPLE',
'CLAUDE_CODE_DISABLE_THINKING',
'DISABLE_INTERLEAVED_THINKING',
'DISABLE_COMPACT',
'DISABLE_AUTO_COMPACT',
'CLAUDE_CODE_DISABLE_AUTO_MEMORY',
'CLAUDE_CODE_DISABLE_BACKGROUND_TASKS',
]) {
// eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level
process.env[k] ??= '1'
}
}
/**
* Bootstrap entrypoint - checks for special flags before loading the full CLI.
* All imports are dynamic to minimize module evaluation for fast paths.
* Fast-path for --version has zero imports beyond this file.
*/
async function main(): Promise<void> {
const args = process.argv.slice(2)
// Fast-path for --version/-v: zero module loading needed
if (
args.length === 1 &&
(args[0] === '--version' || args[0] === '-v' || args[0] === '-V')
) {
// MACRO.VERSION is inlined at build time
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`${MACRO.VERSION} (Claude Code)`)
return
}
// For all other paths, load the startup profiler
const { profileCheckpoint } = await import('../utils/startupProfiler.js')
profileCheckpoint('cli_entry')
// Fast-path for --dump-system-prompt: output the rendered system prompt and exit.
// Used by prompt sensitivity evals to extract the system prompt at a specific commit.
// Ant-only: eliminated from external builds via feature flag.
if (feature('DUMP_SYSTEM_PROMPT') && args[0] === '--dump-system-prompt') {
profileCheckpoint('cli_dump_system_prompt_path')
const { enableConfigs } = await import('../utils/config.js')
enableConfigs()
const { getMainLoopModel } = await import('../utils/model/model.js')
const modelIdx = args.indexOf('--model')
const model = (modelIdx !== -1 && args[modelIdx + 1]) || getMainLoopModel()
const { getSystemPrompt } = await import('../constants/prompts.js')
const prompt = await getSystemPrompt([], model)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(prompt.join('\n'))
return
}
if (process.argv[2] === '--claude-in-chrome-mcp') {
profileCheckpoint('cli_claude_in_chrome_mcp_path')
const { runClaudeInChromeMcpServer } = await import(
'../utils/claudeInChrome/mcpServer.js'
)
await runClaudeInChromeMcpServer()
return
} else if (process.argv[2] === '--chrome-native-host') {
profileCheckpoint('cli_chrome_native_host_path')
const { runChromeNativeHost } = await import(
'../utils/claudeInChrome/chromeNativeHost.js'
)
await runChromeNativeHost()
return
} else if (
feature('CHICAGO_MCP') &&
process.argv[2] === '--computer-use-mcp'
) {
profileCheckpoint('cli_computer_use_mcp_path')
const { runComputerUseMcpServer } = await import(
'../utils/computerUse/mcpServer.js'
)
await runComputerUseMcpServer()
return
}
// Fast-path for `--acp` — ACP (Agent Client Protocol) agent mode over stdio.
if (feature('ACP') && process.argv[2] === '--acp') {
profileCheckpoint('cli_acp_path')
const { runAcpAgent } = await import('../services/acp/entry.js')
await runAcpAgent()
return
}
if (args[0] === 'weixin') {
profileCheckpoint('cli_weixin_path')
const { handleWeixinCli } = await import('@claude-code-best/weixin')
const { enableConfigs } = await import('../utils/config.js')
const { initializeAnalyticsSink } = await import('../services/analytics/sink.js')
const { shutdownDatadog } = await import('../services/analytics/datadog.js')
const { shutdown1PEventLogging } = await import('../services/analytics/firstPartyEventLogger.js')
const { logForDebugging } = await import('../utils/debug.js')
const { ChannelPermissionRequestNotificationSchema } = await import('../services/mcp/channelNotification.js')
await handleWeixinCli(args.slice(1), {
enableConfigs,
initializeAnalyticsSink,
shutdownDatadog,
shutdown1PEventLogging,
logForDebugging,
registerPermissionHandler(server, handler) {
server.setNotificationHandler(
ChannelPermissionRequestNotificationSchema(),
async notification => handler(notification.params),
)
},
}, MACRO.VERSION)
return
}
// Fast-path for `--daemon-worker=<kind>` (internal — supervisor spawns this).
// Must come before the daemon subcommand check: spawned per-worker, so
// perf-sensitive. No enableConfigs(), no analytics sinks at this layer —
// workers are lean. If a worker kind needs configs/auth (assistant will),
// it calls them inside its run() fn.
if (feature('DAEMON') && (args[0] === '--daemon-worker' || args[0]?.startsWith('--daemon-worker='))) {
const kind = args[0] === '--daemon-worker' ? args[1] : args[0].split('=')[1]
const { runDaemonWorker } = await import('../daemon/workerRegistry.js')
await runDaemonWorker(kind)
return
}
// Fast-path for `claude remote-control` (also accepts legacy `claude remote` / `claude sync` / `claude bridge`):
// serve local machine as bridge environment.
// feature() must stay inline for build-time dead code elimination;
// isBridgeEnabled() checks the runtime GrowthBook gate.
if (
feature('BRIDGE_MODE') &&
(args[0] === 'remote-control' ||
args[0] === 'rc' ||
args[0] === 'remote' ||
args[0] === 'sync' ||
args[0] === 'bridge')
) {
profileCheckpoint('cli_bridge_path')
const { enableConfigs } = await import('../utils/config.js')
enableConfigs()
const { getBridgeDisabledReason, checkBridgeMinVersion } = await import(
'../bridge/bridgeEnabled.js'
)
const { BRIDGE_LOGIN_ERROR } = await import('../bridge/types.js')
const { bridgeMain } = await import('../bridge/bridgeMain.js')
const { exitWithError } = await import('../utils/process.js')
// Auth check must come before the GrowthBook gate check — without auth,
// GrowthBook has no user context and would return a stale/default false.
// getBridgeDisabledReason awaits GB init, so the returned value is fresh
// (not the stale disk cache), but init still needs auth headers to work.
const { getClaudeAIOAuthTokens } = await import('../utils/auth.js')
const { getBridgeAccessToken } = await import('../bridge/bridgeConfig.js')
if (!getClaudeAIOAuthTokens()?.accessToken && !getBridgeAccessToken()) {
exitWithError(BRIDGE_LOGIN_ERROR)
}
const disabledReason = await getBridgeDisabledReason()
if (disabledReason) {
exitWithError(`Error: ${disabledReason}`)
}
const versionError = checkBridgeMinVersion()
if (versionError) {
exitWithError(versionError)
}
// Bridge is a remote control feature - check policy limits
const { waitForPolicyLimitsToLoad, isPolicyAllowed } = await import(
'../services/policyLimits/index.js'
)
await waitForPolicyLimitsToLoad()
if (!isPolicyAllowed('allow_remote_control')) {
exitWithError(
"Error: Remote Control is disabled by your organization's policy.",
)
}
await bridgeMain(args.slice(1))
return
}
// Fast-path for `claude daemon [subcommand]`: unified daemon + session management.
// Handles both supervisor (start/stop) and background session (bg/attach/logs/kill)
// subcommands under one namespace.
if (
(feature('DAEMON') || feature('BG_SESSIONS')) &&
args[0] === 'daemon'
) {
profileCheckpoint('cli_daemon_path')
const { enableConfigs } = await import('../utils/config.js')
enableConfigs()
const { setShellIfWindows } = await import('../utils/windowsPaths.js')
setShellIfWindows()
const { initSinks } = await import('../utils/sinks.js')
initSinks()
const { daemonMain } = await import('../daemon/main.js')
await daemonMain(args.slice(1))
return
}
// Fast-path for `--bg`/`--background` shortcut → daemon bg.
if (
feature('BG_SESSIONS') &&
(args.includes('--bg') || args.includes('--background'))
) {
profileCheckpoint('cli_daemon_path')
const { enableConfigs } = await import('../utils/config.js')
enableConfigs()
const { setShellIfWindows } = await import('../utils/windowsPaths.js')
setShellIfWindows()
const bg = await import('../cli/bg.js')
await bg.handleBgStart(
args.filter(a => a !== '--bg' && a !== '--background'),
)
return
}
// Backward-compat: ps/logs/attach/kill → daemon <sub> (deprecated)
if (
feature('BG_SESSIONS') &&
(args[0] === 'ps' ||
args[0] === 'logs' ||
args[0] === 'attach' ||
args[0] === 'kill')
) {
const mapped = args[0] === 'ps' ? 'status' : args[0]
console.error(
`[deprecated] Use: claude daemon ${mapped}${args[1] ? ' ' + args[1] : ''}`,
)
profileCheckpoint('cli_daemon_path')
const { enableConfigs } = await import('../utils/config.js')
enableConfigs()
const { setShellIfWindows } = await import('../utils/windowsPaths.js')
setShellIfWindows()
const { initSinks } = await import('../utils/sinks.js')
initSinks()
const { daemonMain } = await import('../daemon/main.js')
await daemonMain([args[0] === 'ps' ? 'status' : args[0]!, ...args.slice(1)])
return
}
// Fast-path for `claude job <subcommand>`: template jobs.
if (feature('TEMPLATES') && args[0] === 'job') {
profileCheckpoint('cli_templates_path')
const { templatesMain } = await import('../cli/handlers/templateJobs.js')
await templatesMain(args.slice(1))
// process.exit (not return) — mountFleetView's Ink TUI can leave event
// loop handles that prevent natural exit.
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(0)
}
// Backward-compat: new/list/reply → job <sub> (deprecated)
if (
feature('TEMPLATES') &&
(args[0] === 'new' || args[0] === 'list' || args[0] === 'reply')
) {
console.error(
`[deprecated] Use: claude job ${args[0]} ${args.slice(1).join(' ')}`.trim(),
)
profileCheckpoint('cli_templates_path')
const { templatesMain } = await import('../cli/handlers/templateJobs.js')
await templatesMain(args)
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(0)
}
// Fast-path for `claude environment-runner`: headless BYOC runner.
// feature() must stay inline for build-time dead code elimination.
if (feature('BYOC_ENVIRONMENT_RUNNER') && args[0] === 'environment-runner') {
profileCheckpoint('cli_environment_runner_path')
const { environmentRunnerMain } = await import(
'../environment-runner/main.js'
)
await environmentRunnerMain(args.slice(1))
return
}
// Fast-path for `claude self-hosted-runner`: headless self-hosted-runner
// targeting the SelfHostedRunnerWorkerService API (register + poll; poll IS
// heartbeat). feature() must stay inline for build-time dead code elimination.
if (feature('SELF_HOSTED_RUNNER') && args[0] === 'self-hosted-runner') {
profileCheckpoint('cli_self_hosted_runner_path')
const { selfHostedRunnerMain } = await import(
'../self-hosted-runner/main.js'
)
await selfHostedRunnerMain(args.slice(1))
return
}
// Fast-path for --worktree --tmux: exec into tmux before loading full CLI
const hasTmuxFlag = args.includes('--tmux') || args.includes('--tmux=classic')
if (
hasTmuxFlag &&
(args.includes('-w') ||
args.includes('--worktree') ||
args.some(a => a.startsWith('--worktree=')))
) {
profileCheckpoint('cli_tmux_worktree_fast_path')
const { enableConfigs } = await import('../utils/config.js')
enableConfigs()
const { isWorktreeModeEnabled } = await import(
'../utils/worktreeModeEnabled.js'
)
if (isWorktreeModeEnabled()) {
const { execIntoTmuxWorktree } = await import('../utils/worktree.js')
const result = await execIntoTmuxWorktree(args)
if (result.handled) {
return
}
// If not handled (e.g., error), fall through to normal CLI
if (result.error) {
const { exitWithError } = await import('../utils/process.js')
exitWithError(result.error)
}
}
}
// Redirect common update flag mistakes to the update subcommand
if (
args.length === 1 &&
(args[0] === '--update' || args[0] === '--upgrade')
) {
process.argv = [process.argv[0]!, process.argv[1]!, 'update']
}
// --bare: set SIMPLE early so gates fire during module eval / commander
// option building (not just inside the action handler).
if (args.includes('--bare')) {
process.env.CLAUDE_CODE_SIMPLE = '1'
}
// No special flags detected, load and run the full CLI
const { startCapturingEarlyInput } = await import('../utils/earlyInput.js')
startCapturingEarlyInput()
profileCheckpoint('cli_before_main_import')
const { main: cliMain } = await import('../main.jsx')
profileCheckpoint('cli_after_main_import')
await cliMain()
profileCheckpoint('cli_after_main_complete')
}
// eslint-disable-next-line custom-rules/no-top-level-side-effects
void main()