在 CoStrict provider 的所有 API 请求中注入 agent-type header,
反映当前会话所使用的 agent 或 skill,默认值为 "build"。
- fetch.ts: createCoStrictFetch 接受可选 agentType 参数,PascalCase
自动转 kebab-case(StrictSpec → strict-spec),注入 agent-type header
- index.ts: queryModelCoStrict 读取 getMainThreadAgentType()(--agent 启动)
或 getActiveSkillName()(slash skill 触发)作为 agentType
- models.ts: /v1/models 请求添加 User-Agent: csc/{VERSION} header
- state.ts: 新增 activeSkillName 状态及 getter/setter
- processSlashCommand.tsx: inline skill 和 fork skill(context:fork)
触发时均调用 setActiveSkillName 记录当前 skill/agent 名
- caches.ts: /clear 时调用 setActiveSkillName(undefined) 重置状态,
避免新会话继承上一会话的 agent-type
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
165 lines
5.3 KiB
TypeScript
165 lines
5.3 KiB
TypeScript
/**
|
||
* CoStrict 自定义 fetch 模块
|
||
* 动态注入 Authorization header 并处理 token 刷新
|
||
*/
|
||
|
||
import { randomUUID } from 'node:crypto'
|
||
import {
|
||
loadCoStrictCredentials,
|
||
saveCoStrictCredentials,
|
||
} from './credentials.js'
|
||
import {
|
||
isCoStrictTokenValid,
|
||
refreshCoStrictToken,
|
||
extractExpiryFromJWT,
|
||
} from './token.js'
|
||
|
||
import { createRequire } from 'module'
|
||
|
||
function getVersion(): string {
|
||
try {
|
||
if (typeof MACRO !== 'undefined' && MACRO.VERSION) return MACRO.VERSION
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
const require = createRequire(import.meta.url)
|
||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||
const pkg = require('../../../package.json') as { version: string }
|
||
return pkg.version
|
||
} catch {
|
||
return '1.0.0'
|
||
}
|
||
}
|
||
|
||
const VERSION = getVersion()
|
||
|
||
type CoStrictFetch = typeof fetch & {
|
||
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 请求
|
||
*
|
||
* 功能:
|
||
* 1. 动态读取凭证
|
||
* 2. 预防性 Token 刷新(请求前检查)
|
||
* 3. 注入 Authorization 和 CoStrict 特有 headers
|
||
* 4. 反应性 401 错误恢复(自动重试一次)
|
||
*/
|
||
export function createCoStrictFetch(options?: {
|
||
agentType?: string
|
||
}): CoStrictFetch {
|
||
const agentType = toKebabCase(options?.agentType) || 'build'
|
||
const costrictFetch = async (
|
||
input: RequestInfo | URL,
|
||
init?: RequestInit,
|
||
) => {
|
||
// ========== 步骤 1: 动态读取凭证 ==========
|
||
let creds = await loadCoStrictCredentials()
|
||
|
||
if (!creds) {
|
||
throw new Error(
|
||
'CoStrict credentials not found. Please run /costrict-login first.',
|
||
)
|
||
}
|
||
|
||
// ========== 步骤 2: 预防性 Token 刷新 ==========
|
||
if (creds.refresh_token && !isCoStrictTokenValid(creds)) {
|
||
try {
|
||
const refreshed = await refreshCoStrictToken({
|
||
baseUrl: creds.base_url,
|
||
refreshToken: creds.refresh_token,
|
||
state: creds.state,
|
||
})
|
||
const updatedCreds = {
|
||
...creds,
|
||
access_token: refreshed.access_token,
|
||
refresh_token: refreshed.refresh_token,
|
||
expiry_date: extractExpiryFromJWT(refreshed.access_token),
|
||
updated_at: new Date().toISOString(),
|
||
expired_at: new Date(
|
||
extractExpiryFromJWT(refreshed.access_token),
|
||
).toISOString(),
|
||
}
|
||
await saveCoStrictCredentials(updatedCreds)
|
||
creds = updatedCreds
|
||
} catch {
|
||
// 刷新失败,继续使用旧 token 尝试
|
||
}
|
||
}
|
||
|
||
// ========== 步骤 3: 构建 headers ==========
|
||
const headers = new Headers(init?.headers)
|
||
headers.set('Authorization', `Bearer ${creds.access_token}`)
|
||
headers.set('User-Agent', `csc/${VERSION}`)
|
||
headers.set('HTTP-Referer', 'https://github.com/zgsm-ai/costrict-cli')
|
||
headers.set('X-Title', 'CoStrict-CLI')
|
||
headers.set('X-Costrict-Version', `costrict-cli-${VERSION}`)
|
||
headers.set('X-Request-ID', randomUUID())
|
||
headers.set('agent-type', agentType)
|
||
headers.set('zgsm-client-id', creds.machine_id)
|
||
headers.set('zgsm-client-ide', 'cli')
|
||
|
||
// ========== 步骤 4: 发起请求 ==========
|
||
const response = await fetch(input, { ...init, headers })
|
||
|
||
// ========== 步骤 5: 反应性 401 恢复 ==========
|
||
if (response.status === 401 && creds.refresh_token) {
|
||
try {
|
||
const refreshed = await refreshCoStrictToken({
|
||
baseUrl: creds.base_url,
|
||
refreshToken: creds.refresh_token,
|
||
state: creds.state,
|
||
})
|
||
const updatedCreds = {
|
||
...creds,
|
||
access_token: refreshed.access_token,
|
||
refresh_token: refreshed.refresh_token,
|
||
expiry_date: extractExpiryFromJWT(refreshed.access_token),
|
||
updated_at: new Date().toISOString(),
|
||
expired_at: new Date(
|
||
extractExpiryFromJWT(refreshed.access_token),
|
||
).toISOString(),
|
||
}
|
||
await saveCoStrictCredentials(updatedCreds)
|
||
headers.set('Authorization', `Bearer ${refreshed.access_token}`)
|
||
headers.set('X-Request-ID', randomUUID())
|
||
return fetch(input, { ...init, headers })
|
||
} catch {
|
||
// 重试失败,返回原始 401 响应
|
||
}
|
||
}
|
||
|
||
return response
|
||
}
|
||
// Bun 原生支持 fetch.preconnect(共享连接池预热)
|
||
// Node.js 没有 preconnect API,降级为 net.createConnection 做 TCP 预热
|
||
const costrictFetchWithPreconnect = Object.assign(costrictFetch, {
|
||
preconnect: typeof fetch.preconnect === 'function'
|
||
? fetch.preconnect.bind(fetch)
|
||
: (url: string | URL) => {
|
||
try {
|
||
const { hostname, port } = new URL(typeof url === 'string' ? url : url.toString())
|
||
const { createConnection } = require('node:net') as typeof import('node:net')
|
||
const sock = createConnection(Number(port) || 443, hostname, () => sock.destroy())
|
||
sock.setTimeout(3000, () => sock.destroy())
|
||
sock.on('error', () => sock.destroy())
|
||
} catch {
|
||
// 预连接失败不影响正常流程
|
||
}
|
||
}
|
||
})
|
||
|
||
return costrictFetchWithPreconnect as CoStrictFetch
|
||
}
|