Merge remote-tracking branch 'origin/main' into feat/migrate-review-agents
This commit is contained in:
commit
37f53440f3
|
|
@ -77,7 +77,8 @@
|
||||||
"@agentclientprotocol/sdk": "^0.19.0",
|
"@agentclientprotocol/sdk": "^0.19.0",
|
||||||
"@claude-code-best/mcp-chrome-bridge": "^2.0.8",
|
"@claude-code-best/mcp-chrome-bridge": "^2.0.8",
|
||||||
"ws": "^8.20.0",
|
"ws": "^8.20.0",
|
||||||
"gray-matter": "^4.0.3"
|
"gray-matter": "^4.0.3",
|
||||||
|
"undici": "^7.24.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@alcalzone/ansi-tokenize": "^0.3.0",
|
"@alcalzone/ansi-tokenize": "^0.3.0",
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,8 @@ type State = {
|
||||||
sdkBetas: string[] | undefined
|
sdkBetas: string[] | undefined
|
||||||
// Main thread agent type (from --agent flag or settings)
|
// Main thread agent type (from --agent flag or settings)
|
||||||
mainThreadAgentType: string | undefined
|
mainThreadAgentType: string | undefined
|
||||||
|
// Currently active skill name (set when a /skill-name is invoked)
|
||||||
|
activeSkillName: string | undefined
|
||||||
// Remote mode (--remote flag)
|
// Remote mode (--remote flag)
|
||||||
isRemoteMode: boolean
|
isRemoteMode: boolean
|
||||||
// Direct connect server URL (for display in header)
|
// Direct connect server URL (for display in header)
|
||||||
|
|
@ -381,6 +383,8 @@ function getInitialState(): State {
|
||||||
sdkBetas: undefined,
|
sdkBetas: undefined,
|
||||||
// Main thread agent type
|
// Main thread agent type
|
||||||
mainThreadAgentType: undefined,
|
mainThreadAgentType: undefined,
|
||||||
|
// Currently active skill name
|
||||||
|
activeSkillName: undefined,
|
||||||
// Remote mode
|
// Remote mode
|
||||||
isRemoteMode: false,
|
isRemoteMode: false,
|
||||||
...(process.env.USER_TYPE === 'ant'
|
...(process.env.USER_TYPE === 'ant'
|
||||||
|
|
@ -1622,6 +1626,14 @@ export function setMainThreadAgentType(agentType: string | undefined): void {
|
||||||
STATE.mainThreadAgentType = agentType
|
STATE.mainThreadAgentType = agentType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getActiveSkillName(): string | undefined {
|
||||||
|
return STATE.activeSkillName
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setActiveSkillName(skillName: string | undefined): void {
|
||||||
|
STATE.activeSkillName = skillName
|
||||||
|
}
|
||||||
|
|
||||||
export function getIsRemoteMode(): boolean {
|
export function getIsRemoteMode(): boolean {
|
||||||
return STATE.isRemoteMode
|
return STATE.isRemoteMode
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import { feature } from 'bun:bundle'
|
import { feature } from 'bun:bundle'
|
||||||
import {
|
import {
|
||||||
clearInvokedSkills,
|
clearInvokedSkills,
|
||||||
|
setActiveSkillName,
|
||||||
setLastEmittedDate,
|
setLastEmittedDate,
|
||||||
} from '../../bootstrap/state.js'
|
} from '../../bootstrap/state.js'
|
||||||
import { clearCommandsCache } from '../../commands.js'
|
import { clearCommandsCache } from '../../commands.js'
|
||||||
|
|
@ -115,6 +116,8 @@ export function clearSessionCaches(
|
||||||
if (!hasPreserved) clearAllDumpState()
|
if (!hasPreserved) clearAllDumpState()
|
||||||
// Clear invoked skills cache (each entry holds full skill file content)
|
// Clear invoked skills cache (each entry holds full skill file content)
|
||||||
clearInvokedSkills(preservedAgentIds)
|
clearInvokedSkills(preservedAgentIds)
|
||||||
|
// Reset active skill name so new session doesn't inherit previous session's agent-type
|
||||||
|
setActiveSkillName(undefined)
|
||||||
// Clear git dir resolution cache
|
// Clear git dir resolution cache
|
||||||
clearResolveGitDirCache()
|
clearResolveGitDirCache()
|
||||||
// Clear dynamic skills (loaded from skill directories)
|
// Clear dynamic skills (loaded from skill directories)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
// Critical system constants extracted to break circular dependencies
|
// Critical system constants extracted to break circular dependencies
|
||||||
|
|
||||||
import { feature } from 'bun:bundle'
|
|
||||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
|
|
||||||
import { logForDebugging } from '../utils/debug.js'
|
|
||||||
import { isEnvDefinedFalsy } from '../utils/envUtils.js'
|
|
||||||
import { getAPIProvider } from '../utils/model/providers.js'
|
import { getAPIProvider } from '../utils/model/providers.js'
|
||||||
import { getWorkload } from '../utils/workloadContext.js'
|
|
||||||
|
|
||||||
const DEFAULT_PREFIX = `You are Claude Code, Anthropic's official CLI for Claude.`
|
const DEFAULT_PREFIX = `You are Claude Code, Anthropic's official CLI for Claude.`
|
||||||
const AGENT_SDK_CLAUDE_CODE_PRESET_PREFIX = `You are Claude Code, Anthropic's official CLI for Claude, running within the Claude Agent SDK.`
|
const AGENT_SDK_CLAUDE_CODE_PRESET_PREFIX = `You are Claude Code, Anthropic's official CLI for Claude, running within the Claude Agent SDK.`
|
||||||
|
|
@ -45,51 +40,6 @@ export function getCLISyspromptPrefix(options?: {
|
||||||
return DEFAULT_PREFIX
|
return DEFAULT_PREFIX
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function getAttributionHeader(_fingerprint: string): string {
|
||||||
* Check if attribution header is enabled.
|
return ''
|
||||||
* Enabled by default, can be disabled via env var or GrowthBook killswitch.
|
|
||||||
*/
|
|
||||||
function isAttributionHeaderEnabled(): boolean {
|
|
||||||
if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_ATTRIBUTION_HEADER)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_attribution_header', true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get attribution header for API requests.
|
|
||||||
* Returns a header string with cc_version (including fingerprint) and cc_entrypoint.
|
|
||||||
* Enabled by default, can be disabled via env var or GrowthBook killswitch.
|
|
||||||
*
|
|
||||||
* When NATIVE_CLIENT_ATTESTATION is enabled, includes a `cch=00000` placeholder.
|
|
||||||
* Before the request is sent, Bun's native HTTP stack finds this placeholder
|
|
||||||
* in the request body and overwrites the zeros with a computed hash. The
|
|
||||||
* server verifies this token to confirm the request came from a real Claude
|
|
||||||
* Code client. See bun-anthropic/src/http/Attestation.zig for implementation.
|
|
||||||
*
|
|
||||||
* We use a placeholder (instead of injecting from Zig) because same-length
|
|
||||||
* replacement avoids Content-Length changes and buffer reallocation.
|
|
||||||
*/
|
|
||||||
export function getAttributionHeader(fingerprint: string): string {
|
|
||||||
if (!isAttributionHeaderEnabled()) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const version = `${MACRO.VERSION}.${fingerprint}`
|
|
||||||
const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT ?? 'unknown'
|
|
||||||
|
|
||||||
// cch=00000 placeholder is overwritten by Bun's HTTP stack with attestation token
|
|
||||||
const cch = feature('NATIVE_CLIENT_ATTESTATION') ? ' cch=00000;' : ''
|
|
||||||
// cc_workload: turn-scoped hint so the API can route e.g. cron-initiated
|
|
||||||
// requests to a lower QoS pool. Absent = interactive default. Safe re:
|
|
||||||
// fingerprint (computed from msg chars + version only, line 78 above) and
|
|
||||||
// cch attestation (placeholder overwritten in serialized body bytes after
|
|
||||||
// this string is built). Server _parse_cc_header tolerates unknown extra
|
|
||||||
// fields so old API deploys silently ignore this.
|
|
||||||
const workload = getWorkload()
|
|
||||||
const workloadPair = workload ? ` cc_workload=${workload};` : ''
|
|
||||||
const header = `x-anthropic-billing-header: cc_version=${version}; cc_entrypoint=${entrypoint};${cch}${workloadPair}`
|
|
||||||
|
|
||||||
logForDebugging(`attribution header ${header}`)
|
|
||||||
return header
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,15 @@ type CoStrictFetch = typeof fetch & {
|
||||||
preconnect?: (url: string | URL) => void
|
preconnect?: (url: string | URL) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PascalCase / camelCase → kebab-case: "StrictSpec" → "strict-spec", "TDD" → "tdd"
|
||||||
|
function toKebabCase(s: string | undefined): string | undefined {
|
||||||
|
if (!s) return undefined
|
||||||
|
return s
|
||||||
|
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
||||||
|
.replace(/([a-z\d])([A-Z])/g, '$1-$2')
|
||||||
|
.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建自定义 fetch 函数,用于 CoStrict API 请求
|
* 创建自定义 fetch 函数,用于 CoStrict API 请求
|
||||||
*
|
*
|
||||||
|
|
@ -47,7 +56,10 @@ type CoStrictFetch = typeof fetch & {
|
||||||
* 3. 注入 Authorization 和 CoStrict 特有 headers
|
* 3. 注入 Authorization 和 CoStrict 特有 headers
|
||||||
* 4. 反应性 401 错误恢复(自动重试一次)
|
* 4. 反应性 401 错误恢复(自动重试一次)
|
||||||
*/
|
*/
|
||||||
export function createCoStrictFetch(): CoStrictFetch {
|
export function createCoStrictFetch(options?: {
|
||||||
|
agentType?: string
|
||||||
|
}): CoStrictFetch {
|
||||||
|
const agentType = toKebabCase(options?.agentType) || 'build'
|
||||||
const costrictFetch = async (
|
const costrictFetch = async (
|
||||||
input: RequestInfo | URL,
|
input: RequestInfo | URL,
|
||||||
init?: RequestInit,
|
init?: RequestInit,
|
||||||
|
|
@ -94,6 +106,7 @@ export function createCoStrictFetch(): CoStrictFetch {
|
||||||
headers.set('X-Title', 'CoStrict-CLI')
|
headers.set('X-Title', 'CoStrict-CLI')
|
||||||
headers.set('X-Costrict-Version', `costrict-cli-${VERSION}`)
|
headers.set('X-Costrict-Version', `costrict-cli-${VERSION}`)
|
||||||
headers.set('X-Request-ID', randomUUID())
|
headers.set('X-Request-ID', randomUUID())
|
||||||
|
headers.set('agent-type', agentType)
|
||||||
headers.set('zgsm-client-id', creds.machine_id)
|
headers.set('zgsm-client-id', creds.machine_id)
|
||||||
headers.set('zgsm-client-ide', 'cli')
|
headers.set('zgsm-client-ide', 'cli')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ import { resolveCoStrictModel } from './modelMapping.js'
|
||||||
import { getCoStrictBaseURL } from './auth.js'
|
import { getCoStrictBaseURL } from './auth.js'
|
||||||
import { loadCoStrictCredentials } from './credentials.js'
|
import { loadCoStrictCredentials } from './credentials.js'
|
||||||
import { isOpenAIThinkingEnabled } from '../../services/api/openai/requestBody.js'
|
import { isOpenAIThinkingEnabled } from '../../services/api/openai/requestBody.js'
|
||||||
|
import { fetchCoStrictModels } from './models.js'
|
||||||
|
import { getMainThreadAgentType, getActiveSkillName } from '../../bootstrap/state.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CoStrict 查询路径
|
* CoStrict 查询路径
|
||||||
|
|
@ -57,10 +59,28 @@ export async function* queryModelCoStrict(
|
||||||
const baseUrl = getCoStrictBaseURL(creds?.base_url)
|
const baseUrl = getCoStrictBaseURL(creds?.base_url)
|
||||||
const chatBaseURL = `${baseUrl}/chat-rag/api/v1`
|
const chatBaseURL = `${baseUrl}/chat-rag/api/v1`
|
||||||
|
|
||||||
// 3. 规范化消息
|
// 3. 从模型列表获取 maxTokens 相关参数
|
||||||
|
let maxTokensParamKey: string = 'max_tokens'
|
||||||
|
let maxTokensValue: number | undefined
|
||||||
|
if (creds?.access_token) {
|
||||||
|
try {
|
||||||
|
const modelList = await fetchCoStrictModels(baseUrl, creds.access_token)
|
||||||
|
const modelInfo = modelList.find(m => m.id === costrictModel)
|
||||||
|
if (modelInfo) {
|
||||||
|
maxTokensParamKey = modelInfo.maxTokensKey || 'max_tokens'
|
||||||
|
if (modelInfo.maxTokens != null) {
|
||||||
|
maxTokensValue = modelInfo.maxTokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 获取模型列表失败,使用默认值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 规范化消息
|
||||||
const messagesForAPI = normalizeMessagesForAPI(messages, tools)
|
const messagesForAPI = normalizeMessagesForAPI(messages, tools)
|
||||||
|
|
||||||
// 4. 构建工具 schema
|
// 5. 构建工具 schema
|
||||||
const toolSchemas = await Promise.all(
|
const toolSchemas = await Promise.all(
|
||||||
tools.map(tool =>
|
tools.map(tool =>
|
||||||
toolToAPISchema(tool, {
|
toolToAPISchema(tool, {
|
||||||
|
|
@ -81,7 +101,7 @@ export async function* queryModelCoStrict(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// 5. 转换为 OpenAI 格式
|
// 6. 转换为 OpenAI 格式
|
||||||
// 根据模型名称自动检测是否启用thinking模式
|
// 根据模型名称自动检测是否启用thinking模式
|
||||||
const enableThinking = isOpenAIThinkingEnabled(costrictModel)
|
const enableThinking = isOpenAIThinkingEnabled(costrictModel)
|
||||||
const openaiMessages = anthropicMessagesToOpenAI(
|
const openaiMessages = anthropicMessagesToOpenAI(
|
||||||
|
|
@ -92,8 +112,10 @@ export async function* queryModelCoStrict(
|
||||||
const openaiTools = anthropicToolsToOpenAI(standardTools)
|
const openaiTools = anthropicToolsToOpenAI(standardTools)
|
||||||
const openaiToolChoice = anthropicToolChoiceToOpenAI(options.toolChoice)
|
const openaiToolChoice = anthropicToolChoiceToOpenAI(options.toolChoice)
|
||||||
|
|
||||||
// 6. 创建专用的 CoStrict OpenAI 客户端(不缓存,每次使用新的 fetch)
|
// 7. 创建专用的 CoStrict OpenAI 客户端(不缓存,每次使用新的 fetch)
|
||||||
const costrictFetch = createCoStrictFetch()
|
const costrictFetch = createCoStrictFetch({
|
||||||
|
agentType: getMainThreadAgentType() ?? getActiveSkillName(),
|
||||||
|
})
|
||||||
const client = new OpenAI({
|
const client = new OpenAI({
|
||||||
apiKey: 'costrict-managed', // 实际 token 由 createCoStrictFetch 注入
|
apiKey: 'costrict-managed', // 实际 token 由 createCoStrictFetch 注入
|
||||||
baseURL: chatBaseURL,
|
baseURL: chatBaseURL,
|
||||||
|
|
@ -111,28 +133,32 @@ export async function* queryModelCoStrict(
|
||||||
`[CoStrict] model=${costrictModel}, baseURL=${chatBaseURL}, messages=${openaiMessages.length}, tools=${openaiTools.length}`,
|
`[CoStrict] model=${costrictModel}, baseURL=${chatBaseURL}, messages=${openaiMessages.length}, tools=${openaiTools.length}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 7. 调用 API(流式)
|
// 8. 调用 API(流式)
|
||||||
|
const requestBody: Record<string, unknown> = {
|
||||||
|
model: costrictModel,
|
||||||
|
messages: openaiMessages,
|
||||||
|
...(openaiTools.length > 0 && {
|
||||||
|
tools: openaiTools,
|
||||||
|
...(openaiToolChoice && {
|
||||||
|
tool_choice:
|
||||||
|
openaiToolChoice as OpenAI.Chat.Completions.ChatCompletionToolChoiceOption,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
stream: true,
|
||||||
|
stream_options: { include_usage: true },
|
||||||
|
...(options.temperatureOverride !== undefined && {
|
||||||
|
temperature: options.temperatureOverride,
|
||||||
|
}),
|
||||||
|
...(maxTokensValue != null && {
|
||||||
|
[maxTokensParamKey]: maxTokensValue,
|
||||||
|
}),
|
||||||
|
}
|
||||||
const stream = await client.chat.completions.create(
|
const stream = await client.chat.completions.create(
|
||||||
{
|
requestBody as unknown as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming,
|
||||||
model: costrictModel,
|
|
||||||
messages: openaiMessages,
|
|
||||||
...(openaiTools.length > 0 && {
|
|
||||||
tools: openaiTools,
|
|
||||||
...(openaiToolChoice && {
|
|
||||||
tool_choice:
|
|
||||||
openaiToolChoice as OpenAI.Chat.Completions.ChatCompletionToolChoiceOption,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
stream: true,
|
|
||||||
stream_options: { include_usage: true },
|
|
||||||
...(options.temperatureOverride !== undefined && {
|
|
||||||
temperature: options.temperatureOverride,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{ signal },
|
{ signal },
|
||||||
)
|
)
|
||||||
|
|
||||||
// 8. 转换流并 yield 事件
|
// 9. 转换流并 yield 事件
|
||||||
const adaptedStream = adaptOpenAIStreamToAnthropic(stream, costrictModel)
|
const adaptedStream = adaptOpenAIStreamToAnthropic(stream, costrictModel)
|
||||||
|
|
||||||
const contentBlocks: Record<number, any> = {}
|
const contentBlocks: Record<number, any> = {}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export interface CoStrictModel {
|
||||||
supportsImages?: boolean
|
supportsImages?: boolean
|
||||||
contextWindow?: number
|
contextWindow?: number
|
||||||
maxTokens?: number
|
maxTokens?: number
|
||||||
|
maxTokensKey?: string
|
||||||
creditConsumption?: number
|
creditConsumption?: number
|
||||||
creditDiscount?: number
|
creditDiscount?: number
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
|
|
@ -42,6 +43,7 @@ export async function fetchCoStrictModels(
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
'User-Agent': `csc/${MACRO.VERSION}`,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import type {
|
||||||
ProgressMessage,
|
ProgressMessage,
|
||||||
UserMessage,
|
UserMessage,
|
||||||
} from 'src/types/message.js'
|
} from 'src/types/message.js'
|
||||||
import { addInvokedSkill, getSessionId } from '../../bootstrap/state.js'
|
import { addInvokedSkill, getSessionId, setActiveSkillName } from '../../bootstrap/state.js'
|
||||||
import { COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG } from '../../constants/xml.js'
|
import { COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG } from '../../constants/xml.js'
|
||||||
import type { CanUseToolFn } from '../../hooks/useCanUseTool.js'
|
import type { CanUseToolFn } from '../../hooks/useCanUseTool.js'
|
||||||
import {
|
import {
|
||||||
|
|
@ -162,6 +162,8 @@ async function executeForkedSlashCommand(
|
||||||
? { ...baseAgent, effort: command.effort }
|
? { ...baseAgent, effort: command.effort }
|
||||||
: baseAgent
|
: baseAgent
|
||||||
|
|
||||||
|
setActiveSkillName(agentDefinition.agentType ?? command.name)
|
||||||
|
|
||||||
logForDebugging(
|
logForDebugging(
|
||||||
`Executing forked slash command /${command.name} with agent ${agentDefinition.agentType}`,
|
`Executing forked slash command /${command.name} with agent ${agentDefinition.agentType}`,
|
||||||
)
|
)
|
||||||
|
|
@ -1230,6 +1232,7 @@ async function getMessagesForPromptSlashCommand(
|
||||||
skillContent,
|
skillContent,
|
||||||
getAgentContext()?.agentId ?? null,
|
getAgentContext()?.agentId ?? null,
|
||||||
)
|
)
|
||||||
|
setActiveSkillName(command.name)
|
||||||
|
|
||||||
const metadata = formatCommandLoadingMetadata(command, args)
|
const metadata = formatCommandLoadingMetadata(command, args)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user