dsh-tap 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +99 -0
- package/CHANGELOG.md +751 -0
- package/LICENSE +21 -0
- package/README.en.md +59 -0
- package/README.md +256 -0
- package/cordis.patch.yml +385 -0
- package/core/bridge.js +698 -0
- package/core/json-store.js +91 -0
- package/core/rotation.js +108 -0
- package/core/usage-meter.js +176 -0
- package/docs/diagnosis-cache-quota.md +181 -0
- package/docs/diagnosis-qoder-flash.md +67 -0
- package/docs/diagnosis-trae-3003.md +302 -0
- package/docs/goals/bridge-port-host-split.md +91 -0
- package/docs/goals/desktop-adaptation.md +106 -0
- package/docs/goals/qoder-cn-provider-design.md +308 -0
- package/docs/goals/settings-card-ux-redesign-plan.md +1680 -0
- package/docs/goals/settings-card-ux-redesign.md +162 -0
- package/docs/goals/trae-agent-v3.md +41 -0
- package/docs/goals/trae-work-cn-repair.md +44 -0
- package/docs/goals/v0.8-/351/242/235/345/272/246/345/217/257/350/247/201-/346/250/241/345/236/213/345/212/250/346/200/201/345/214/226-/345/244/232/346/234/215/345/212/241/345/225/206.md +85 -0
- package/docs/pitfalls.md +105 -0
- package/docs/reverse/trae-cloud-api.md +218 -0
- package/docs/reverse/trae-model-catalog.md +129 -0
- package/docs/reverse/traework-cn.md +520 -0
- package/docs/rules/STATE.md +198 -0
- package/docs/rules/content-moderation.md +54 -0
- package/docs/rules/dev-role-boundary.md +94 -0
- package/docs/rules/extra-providers.md +42 -0
- package/docs/rules/gateway-facts.md +91 -0
- package/docs/rules/oauth-handshake.md +76 -0
- package/docs/rules/prompt-cache.md +93 -0
- package/docs/rules/quota-signals.md +125 -0
- package/docs/rules/routing.md +84 -0
- package/docs/rules/templates/oauth-reverse-checklist.md +42 -0
- package/docs/rules/trae-surface.md +242 -0
- package/docs/rules/ua-validation.md +81 -0
- package/host-config.js +282 -0
- package/index.js +2306 -0
- package/lib/client.js +2893 -0
- package/local-scan.js +104 -0
- package/package.json +82 -0
- package/providers/ark/index.js +11 -0
- package/providers/bailian/index.js +10 -0
- package/providers/bigmodel/index.js +11 -0
- package/providers/codebuddy/agenttool.js +122 -0
- package/providers/codebuddy/catalog.js +227 -0
- package/providers/codebuddy/errors.js +44 -0
- package/providers/codebuddy/headers.js +36 -0
- package/providers/codebuddy/images.js +125 -0
- package/providers/codebuddy/index.js +123 -0
- package/providers/codebuddy/oauth.js +279 -0
- package/providers/deepseek/index.js +11 -0
- package/providers/moonshot/index.js +11 -0
- package/providers/openai-compat.js +177 -0
- package/providers/openrouter/index.js +27 -0
- package/providers/qoder/catalog.js +145 -0
- package/providers/qoder/cosy.js +419 -0
- package/providers/qoder/gateway.js +563 -0
- package/providers/qoder/index.js +116 -0
- package/providers/qoder/oauth.js +364 -0
- package/providers/qoder/qoder_auth.wasm +0 -0
- package/providers/qoder/quota.js +56 -0
- package/providers/qwen/index.js +15 -0
- package/providers/tool-pairing.js +129 -0
- package/providers/trae/catalog.js +103 -0
- package/providers/trae/errors.js +85 -0
- package/providers/trae/gateway.js +853 -0
- package/providers/trae/index.js +126 -0
- package/providers/trae/oauth.js +443 -0
- package/providers/trae/quota.js +75 -0
- package/providers/trae/remote.js +365 -0
- package/scripts/capture-cache.mjs +65 -0
- package/scripts/capture-traffic.mjs +83 -0
- package/scripts/hermes-probe-dev-role.mjs +263 -0
- package/scripts/measure-latency.mjs +253 -0
- package/scripts/probe-ark-thinking.mjs +298 -0
- package/scripts/probe-cache-decline.mjs +292 -0
- package/scripts/probe-cache-ttl.mjs +221 -0
- package/scripts/probe-cache.mjs +156 -0
- package/scripts/probe-codebuddy-efforts.mjs +355 -0
- package/scripts/probe-codebuddy-tier-wiring.mjs +244 -0
- package/scripts/probe-effort-gaps.mjs +133 -0
- package/scripts/probe-media.mjs +115 -0
- package/scripts/probe-moderation.mjs +159 -0
- package/scripts/probe-oauth.mjs +617 -0
- package/scripts/probe-qoder-attribution-arm8.mjs +92 -0
- package/scripts/probe-qoder-attribution-arm9.mjs +102 -0
- package/scripts/probe-qoder-attribution.mjs +266 -0
- package/scripts/probe-qoder-flash-confirm.mjs +94 -0
- package/scripts/probe-qoder-live.mjs +344 -0
- package/scripts/probe-qoder-matrix.mjs +274 -0
- package/scripts/probe-qoder-null-content.mjs +153 -0
- package/scripts/probe-qoder-pairing.mjs +308 -0
- package/scripts/probe-qoder-quota.mjs +162 -0
- package/scripts/probe-qoder-thinking-config.mjs +52 -0
- package/scripts/probe-qoder-thinking-efforts.mjs +269 -0
- package/scripts/probe-quota.mjs +136 -0
- package/scripts/probe-quota2.mjs +144 -0
- package/scripts/probe-routing.mjs +226 -0
- package/scripts/probe-trae-3003-diagnosis.mjs +139 -0
- package/scripts/probe-trae-agent-v3.mjs +399 -0
- package/scripts/probe-trae-efforts.mjs +149 -0
- package/scripts/probe-trae-live.mjs +147 -0
- package/scripts/probe-trae-max-effort.mjs +179 -0
- package/scripts/probe-trae-model-routing.mjs +381 -0
- package/scripts/probe-trae-thinking-scene.mjs +238 -0
- package/scripts/probe-trae-transport-outage.mjs +176 -0
- package/scripts/probe-ua.mjs +508 -0
- package/scripts/trae-model-catalog.mjs +632 -0
- package/scripts/verify-agents-md.mjs +45 -0
- package/scripts/verify-bridge.mjs +1041 -0
- package/scripts/verify-core-generic.mjs +302 -0
- package/scripts/verify-desktop-acceptance.mjs +184 -0
- package/scripts/verify-host-config.mjs +265 -0
- package/scripts/verify-models.mjs +390 -0
- package/scripts/verify-providers.mjs +255 -0
- package/scripts/verify-qoder-provider.mjs +1020 -0
- package/scripts/verify-rotation.mjs +308 -0
- package/scripts/verify-trae-model-catalog.mjs +284 -0
- package/scripts/verify-trae-provider.mjs +1451 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* providers/trae/remote.js — Trae remote 会话传输(chat_sessions 协议)。
|
|
3
|
+
*
|
|
4
|
+
* 存在理由(2026-08-24 探测定论,证据 docs/probes/trae-model-routing[234]-*):
|
|
5
|
+
* raw llm_utils_chat 的模型路由被 function 位钉死——inline_chat 恒走账户默认
|
|
6
|
+
* 模型(非默认模型名一律 3003 "all models failed"),solo_agent_lite 恒
|
|
7
|
+
* seed-code-lite、solo_work_lite 恒 glm-5.2、chat_v3 恒 seed-code-lite;
|
|
8
|
+
* model/custom_model 字段均被忽略。**唯一真实的模型选择机制是 remote 会话
|
|
9
|
+
* 协议**:POST /api/remote/v1/chat_sessions 的 initial_message.model_name +
|
|
10
|
+
* model_selection_strategy:"manual"(solo_agent_remote 组全部模型可选,
|
|
11
|
+
* model_config/done 事件的 config_name 双重证实)。
|
|
12
|
+
*
|
|
13
|
+
* 代价(诚实标注):每请求起一个云端沙箱 agent(25k 系统提示,跨会话有前缀
|
|
14
|
+
* 缓存),消耗 **work 额度池**(available_endpoint=1;raw 通道消耗 IDE 池
|
|
15
|
+
* endpoint=0);并发受套餐 solo_agent_parallel_limit 限制(超额排队,
|
|
16
|
+
* queuing/notification 事件);dsh 侧 OpenAI 工具无法原生传递(远端 agent
|
|
17
|
+
* 自持有工具,参照实现用 XML 提示模拟——本传输 v1 不做,带 tools 的请求
|
|
18
|
+
* 由网关侧明确拒绝)。
|
|
19
|
+
*
|
|
20
|
+
* 事件语法(2026-08-24 真实流校准,证据 docs/probes/trae-remote-stream-glm53.txt):
|
|
21
|
+
* metadata / heartbeat / status_changed / platform_timing / timing_events → 忽略
|
|
22
|
+
* queuing / notification → 排队提示
|
|
23
|
+
* model_config {config_name} → 实际路由模型(真值源)
|
|
24
|
+
* plan_item {id, thought, reasoning_content, tool_call_info} → thought 为可见
|
|
25
|
+
* 文本、reasoning_content 为思考,**均为累计快照**(按 id 分槽前缀差分);
|
|
26
|
+
* tool_call_info.name==="finish" 时 params.summary 为最终答复
|
|
27
|
+
* token_usage {prompt_tokens, completion_tokens, total_tokens, ...} → usage
|
|
28
|
+
* done {status:"completed", user_message_context.model_info.config_name} → 终结
|
|
29
|
+
* error → 错误
|
|
30
|
+
* 参照:github.com/autumnsentiment/Trae2api-cn(trae_remote_client.py / sse.py)。
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const DEFAULT_REMOTE_ORIGIN = 'https://solo.trae.cn'
|
|
34
|
+
const WEB_UA =
|
|
35
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
|
|
36
|
+
'(KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36'
|
|
37
|
+
|
|
38
|
+
/** remote 面请求头(Web 客户端形态;与 raw IDE 头组完全不同的指纹)。 */
|
|
39
|
+
export function remoteWebHeaders(token, { stream = true } = {}) {
|
|
40
|
+
return {
|
|
41
|
+
Authorization: `Cloud-IDE-JWT ${token}`,
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
'X-Trae-Client-Type': 'web',
|
|
44
|
+
'X-Preferenced-Language': 'zh-CN',
|
|
45
|
+
'x-user-region': 'CN',
|
|
46
|
+
Origin: DEFAULT_REMOTE_ORIGIN,
|
|
47
|
+
Referer: DEFAULT_REMOTE_ORIGIN + '/',
|
|
48
|
+
'User-Agent': WEB_UA,
|
|
49
|
+
Accept: stream ? 'text/event-stream' : 'application/json',
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** OpenAI content(字符串或分段数组)→ 纯文本。 */
|
|
54
|
+
function contentToText(content) {
|
|
55
|
+
if (typeof content === 'string') return content
|
|
56
|
+
if (Array.isArray(content)) {
|
|
57
|
+
return content
|
|
58
|
+
.filter((p) => p && typeof p === 'object' && p.type === 'text' && typeof p.text === 'string')
|
|
59
|
+
.map((p) => p.text)
|
|
60
|
+
.join('\n')
|
|
61
|
+
}
|
|
62
|
+
return ''
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* OpenAI messages → remote query 字符串(JSON 数组包一个 text 块)。
|
|
67
|
+
* 多轮历史带角色标记扁平化(镜像参照 flatten_query):[System]/[Assistant]/
|
|
68
|
+
* [Client Tool Call]/[Client Tool Result]——remote 会话每请求新建,历史只能
|
|
69
|
+
* 以文本形式随 query 携带。
|
|
70
|
+
*/
|
|
71
|
+
export function flattenQuery(messages) {
|
|
72
|
+
const parts = []
|
|
73
|
+
for (const m of Array.isArray(messages) ? messages : []) {
|
|
74
|
+
if (!m || typeof m !== 'object') continue
|
|
75
|
+
const role = String(m.role ?? 'user')
|
|
76
|
+
const content = contentToText(m.content)
|
|
77
|
+
if (role === 'system' || role === 'developer') {
|
|
78
|
+
if (content) parts.push(`[System]\n${content}`)
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
if (role === 'assistant') {
|
|
82
|
+
const segs = []
|
|
83
|
+
if (content) segs.push(content)
|
|
84
|
+
for (const c of Array.isArray(m.tool_calls) ? m.tool_calls : []) {
|
|
85
|
+
const fn = c?.function ?? {}
|
|
86
|
+
const id = c?.id ?? 'unknown'
|
|
87
|
+
const args = typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments ?? {})
|
|
88
|
+
segs.push(`[Client Tool Call: ${id} ${fn.name ?? 'unknown'}]\nArguments: ${args}`)
|
|
89
|
+
}
|
|
90
|
+
if (segs.length) parts.push('[Assistant]\n' + segs.join('\n\n'))
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
if (role === 'tool') {
|
|
94
|
+
const id = m.tool_call_id ?? 'unknown'
|
|
95
|
+
const name = m.name ?? 'tool'
|
|
96
|
+
parts.push(`[Client Tool Result: ${id} ${name}]\n${content}`)
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
if (content) parts.push(content)
|
|
100
|
+
}
|
|
101
|
+
return JSON.stringify([{ type: 'text', data: { content: parts.join('\n\n') } }])
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function commonParams() {
|
|
105
|
+
return JSON.stringify({
|
|
106
|
+
language: 'zh-cn', app_language: 'zh-CN', quality: 'stable',
|
|
107
|
+
app_version: '1.0.0.1229', web_id: '', user_identity: 'Free',
|
|
108
|
+
is_freshman: '0', biz_user_id: '', user_unique_id: '',
|
|
109
|
+
scope: 'marscode-cn', tenant: 'marscode', region: 'cn', aiRegion: 'cn',
|
|
110
|
+
is_privacy_mode: 0, privacy_mode: 'off', solo_chat_mode: 'code',
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** chat_sessions 创建体(9router 变体:content 空数组,query 承载全部)。
|
|
115
|
+
* opts.reasoningEffort:目录声明的档位拼写 → initial_message.custom_model
|
|
116
|
+
* .reasoning_effort(官方客户端线缆形态,@byted-icube/solo-lite 551.*.mjs
|
|
117
|
+
* custom_model 合并链取证 + 2026-10-04 probe-trae-thinking-scene 证实接受面;
|
|
118
|
+
* preset 模型 config_source=1)。不带/空串 = 不出 custom_model(既有路径
|
|
119
|
+
* 零变化)。 */
|
|
120
|
+
export function buildRemoteCreateBody(model, messages, { reasoningEffort } = {}) {
|
|
121
|
+
const initialMessage = {
|
|
122
|
+
chat_session_id: '',
|
|
123
|
+
content: [],
|
|
124
|
+
query: flattenQuery(messages),
|
|
125
|
+
model_name: model,
|
|
126
|
+
agent_type: 'solo_agent_remote',
|
|
127
|
+
model_selection_strategy: 'manual',
|
|
128
|
+
common_params: commonParams(),
|
|
129
|
+
}
|
|
130
|
+
if (typeof reasoningEffort === 'string' && reasoningEffort) {
|
|
131
|
+
initialMessage.custom_model = {
|
|
132
|
+
model_name: model,
|
|
133
|
+
config_name: model,
|
|
134
|
+
config_source: 1,
|
|
135
|
+
is_preset: true,
|
|
136
|
+
use_remote_service: true,
|
|
137
|
+
multimodal: false,
|
|
138
|
+
reasoning_effort: reasoningEffort,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
mode: 'code',
|
|
143
|
+
environment_id: 'default',
|
|
144
|
+
initial_message: initialMessage,
|
|
145
|
+
env: 'remote',
|
|
146
|
+
auto_create_project: false,
|
|
147
|
+
origin: 'web',
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 创建 remote 会话。返回 {sessionId, messageId};失败抛带 HTTP/业务码的 Error。
|
|
152
|
+
* 边缘韧性(2026-08-24 故障取证,docs/diagnosis-trae-3003.md):TLB/nginx 存在
|
|
153
|
+
* 节点间路由表漂移——同一时刻带凭据请求可能命中缺 /api/remote/v1 路由的节点,
|
|
154
|
+
* 返回**裸文本 404 "Not Found"(或 WAF 空体 403)**,而业务级错误恒为 JSON。
|
|
155
|
+
* 对这两种"裸非 JSON 拒绝"做一次短退避重试(创建失败不产生会话,幂等安全)。 */
|
|
156
|
+
const EDGE_RETRY_DELAY_MS = 900
|
|
157
|
+
const isEdgeSkewReject = (status, text) =>
|
|
158
|
+
(status === 404 || status === 403) && !text.trimStart().startsWith('{')
|
|
159
|
+
|
|
160
|
+
/** create 会话是短请求(非流式 JSON),必须限时——边缘/本地代理死态时快速
|
|
161
|
+
* 失败而非无限挂起(2026-08-24 本地代理抽风实测)。测试可用 opts.timeoutMs 调短。 */
|
|
162
|
+
const CREATE_TIMEOUT_MS = 20_000
|
|
163
|
+
|
|
164
|
+
export async function createRemoteSession(baseURL, token, model, messages, { timeoutMs, reasoningEffort } = {}) {
|
|
165
|
+
const limit = Number(timeoutMs) > 0 ? Number(timeoutMs) : CREATE_TIMEOUT_MS
|
|
166
|
+
const attemptOnce = async () => {
|
|
167
|
+
const resp = await fetch(`${baseURL}/api/remote/v1/chat_sessions`, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: remoteWebHeaders(token, { stream: false }),
|
|
170
|
+
body: JSON.stringify(buildRemoteCreateBody(model, messages, { reasoningEffort })),
|
|
171
|
+
signal: AbortSignal.timeout(limit),
|
|
172
|
+
})
|
|
173
|
+
return { resp, text: await resp.text() }
|
|
174
|
+
}
|
|
175
|
+
let resp
|
|
176
|
+
let text
|
|
177
|
+
try {
|
|
178
|
+
;({ resp, text } = await attemptOnce())
|
|
179
|
+
if (isEdgeSkewReject(resp.status, text)) {
|
|
180
|
+
await new Promise((r) => setTimeout(r, EDGE_RETRY_DELAY_MS))
|
|
181
|
+
;({ resp, text } = await attemptOnce())
|
|
182
|
+
}
|
|
183
|
+
} catch (e) {
|
|
184
|
+
if (e?.name === 'TimeoutError' || /timeout|timed?\s?out/i.test(String(e?.message ?? ''))) {
|
|
185
|
+
throw new Error(`trae remote create_session 超时(${limit}ms 无响应)——边缘/WAF 拦截或本地代理异常;可稍后重试或改用 inline 通道`)
|
|
186
|
+
}
|
|
187
|
+
throw e
|
|
188
|
+
}
|
|
189
|
+
if (resp.status >= 400) {
|
|
190
|
+
const skew = isEdgeSkewReject(resp.status, text)
|
|
191
|
+
const err = new Error(`trae remote create_session [${resp.status}]: ${text.slice(0, 300)}${skew ? '(边缘节点路由漂移/WAF 拦截,通常数分钟内自愈;可稍后重试或改用 inline 通道)' : ''}`)
|
|
192
|
+
err.status = resp.status
|
|
193
|
+
throw err
|
|
194
|
+
}
|
|
195
|
+
let data
|
|
196
|
+
try { data = JSON.parse(text) } catch {
|
|
197
|
+
throw new Error(`trae remote create_session: non-json response: ${text.slice(0, 200)}`)
|
|
198
|
+
}
|
|
199
|
+
if (data.code != null && data.code !== 0 && !data.data) {
|
|
200
|
+
const err = new Error(`trae remote create_session: ${data.code} ${data.message ?? ''}`.trim())
|
|
201
|
+
err.code = data.code
|
|
202
|
+
throw err
|
|
203
|
+
}
|
|
204
|
+
const payload = data.data ?? data
|
|
205
|
+
const sessionId = String(payload.chat_session_id ?? '')
|
|
206
|
+
const messageId = String(payload.message_id ?? '')
|
|
207
|
+
if (!sessionId || !messageId) throw new Error('trae remote create_session: missing chat_session_id/message_id')
|
|
208
|
+
return { sessionId, messageId }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** 拉取事件流(调用方负责读 response.body 到 EOF)。边缘「收下不回应」时
|
|
212
|
+
* 不能永久挂起——与 inline 面(gateway.js 首字节护栏)同口径:护栏只约束
|
|
213
|
+
* 「连上却不出响应头」的窗口,响应头到达即解绑(AbortController + 手动计时,
|
|
214
|
+
* 而非 AbortSignal.timeout——后者会约束整条 SSE 长流,把正常长会话误杀)。
|
|
215
|
+
* 测试可用 opts.timeoutMs 调短。 */
|
|
216
|
+
const OPEN_EVENTS_TIMEOUT_MS = 20_000
|
|
217
|
+
|
|
218
|
+
export async function openRemoteEvents(baseURL, token, sessionId, messageId, { timeoutMs } = {}) {
|
|
219
|
+
const limit = Number(timeoutMs) > 0 ? Number(timeoutMs) : OPEN_EVENTS_TIMEOUT_MS
|
|
220
|
+
const inbound = new AbortController()
|
|
221
|
+
const timer = setTimeout(() => {
|
|
222
|
+
const err = new Error(`trae remote events 超时(${limit}ms 无首字节)——边缘/WAF 拦截或本地代理异常;可稍后重试或改用 inline 通道`)
|
|
223
|
+
err.name = 'TimeoutError'
|
|
224
|
+
inbound.abort(err)
|
|
225
|
+
}, limit)
|
|
226
|
+
const resp = await fetch(
|
|
227
|
+
`${baseURL}/api/remote/v1/chat_sessions/${sessionId}/events?reply_to_message_id=${messageId}`,
|
|
228
|
+
{ headers: remoteWebHeaders(token), signal: inbound.signal },
|
|
229
|
+
).finally(() => clearTimeout(timer))
|
|
230
|
+
if (resp.status >= 400) {
|
|
231
|
+
const text = await resp.text().catch(() => '')
|
|
232
|
+
const err = new Error(`trae remote events [${resp.status}]: ${text.slice(0, 300)}`)
|
|
233
|
+
err.status = resp.status
|
|
234
|
+
throw err
|
|
235
|
+
}
|
|
236
|
+
return resp
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** 终止会话(best-effort,绝不抛出)。 */
|
|
240
|
+
export async function stopRemoteSession(baseURL, token, sessionId, messageId) {
|
|
241
|
+
if (!sessionId || !messageId) return
|
|
242
|
+
try {
|
|
243
|
+
await fetch(`${baseURL}/api/remote/v1/chat_sessions/${sessionId}/stop`, {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
headers: remoteWebHeaders(token, { stream: false }),
|
|
246
|
+
body: JSON.stringify({ chat_session_id: sessionId, user_message_id: messageId }),
|
|
247
|
+
signal: AbortSignal.timeout(10_000),
|
|
248
|
+
})
|
|
249
|
+
} catch { /* best-effort */ }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** 累计快照 → 增量(前缀扩展,否则回退公共前缀——镜像 Trae2api-cn
|
|
253
|
+
* ProtocolTextAccumulator)。实现唯一归此文件;gateway.js import 并 re-export。 */
|
|
254
|
+
export function cumulativeDelta(previous, current) {
|
|
255
|
+
if (typeof current !== 'string' || current === '') return ''
|
|
256
|
+
if (typeof previous !== 'string' || previous === '') return current
|
|
257
|
+
if (current.startsWith(previous)) return current.slice(previous.length)
|
|
258
|
+
if (previous.startsWith(current)) return ''
|
|
259
|
+
let i = 0
|
|
260
|
+
const limit = Math.min(previous.length, current.length)
|
|
261
|
+
while (i < limit && previous[i] === current[i]) i += 1
|
|
262
|
+
return current.slice(i)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 去尾部空白(finish summary 覆盖判定用)。 */
|
|
266
|
+
function rstripEnd(s) {
|
|
267
|
+
return typeof s === 'string' ? s.replace(/\s+$/, '') : ''
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* remote 事件流解析器。吃 (eventName, dataObj),产 {text?, reasoning?, usage?,
|
|
272
|
+
* actualModel?, queue?, finish?, error?}。文本语义:
|
|
273
|
+
* - 每个 plan_item id 一槽,thought=可见文本、reasoning_content=思考(累计快照差分)
|
|
274
|
+
* - finish 工具调用的 params.summary=最终答复:流结束时若可见文本未覆盖则补发
|
|
275
|
+
* - done/EOF 终结;finish_reason 恒 'stop'
|
|
276
|
+
*/
|
|
277
|
+
export function createRemoteEventParser() {
|
|
278
|
+
const state = {
|
|
279
|
+
slots: new Map(), // planItemId -> {thought, reasoning}
|
|
280
|
+
order: [],
|
|
281
|
+
finalSummary: '',
|
|
282
|
+
usage: null,
|
|
283
|
+
actualModel: null,
|
|
284
|
+
done: false,
|
|
285
|
+
queueSeen: false,
|
|
286
|
+
}
|
|
287
|
+
const streamedAll = () => state.order.map((id) => state.slots.get(id)?.thought ?? '').join('')
|
|
288
|
+
/** finish summary 兜底:可见文本为空→补全文;未以 summary 结尾→追加。 */
|
|
289
|
+
const summaryTail = () => {
|
|
290
|
+
const full = streamedAll()
|
|
291
|
+
if (!state.finalSummary) return ''
|
|
292
|
+
if (!full.trim()) return state.finalSummary
|
|
293
|
+
if (!rstripEnd(full).endsWith(rstripEnd(state.finalSummary))) return '\n\n' + state.finalSummary
|
|
294
|
+
return ''
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
isDone: () => state.done,
|
|
299
|
+
usage: () => state.usage,
|
|
300
|
+
actualModel: () => state.actualModel,
|
|
301
|
+
/** 非流式聚合用的最终可见文本(finish summary 兜底)。 */
|
|
302
|
+
finalText: () => streamedAll() + summaryTail(),
|
|
303
|
+
handle(eventName, obj) {
|
|
304
|
+
if (!obj || typeof obj !== 'object') return {}
|
|
305
|
+
const event = typeof obj.event === 'string' && obj.event ? obj.event : eventName
|
|
306
|
+
if (event === 'error') {
|
|
307
|
+
state.done = true
|
|
308
|
+
return { error: obj }
|
|
309
|
+
}
|
|
310
|
+
if (event === 'model_config') {
|
|
311
|
+
if (typeof obj.config_name === 'string' && obj.config_name) state.actualModel = obj.config_name
|
|
312
|
+
return {}
|
|
313
|
+
}
|
|
314
|
+
if (event === 'token_usage') {
|
|
315
|
+
const u = obj.usage ?? obj
|
|
316
|
+
const num = (k) => (typeof u[k] === 'number' && Number.isFinite(u[k]) ? u[k] : null)
|
|
317
|
+
const usage = {
|
|
318
|
+
prompt_tokens: num('prompt_tokens'),
|
|
319
|
+
completion_tokens: num('completion_tokens'),
|
|
320
|
+
total_tokens: num('total_tokens'),
|
|
321
|
+
}
|
|
322
|
+
if (usage.total_tokens != null || usage.prompt_tokens != null) state.usage = usage
|
|
323
|
+
return {}
|
|
324
|
+
}
|
|
325
|
+
if (event === 'queuing' || event === 'notification') {
|
|
326
|
+
if (!state.queueSeen) {
|
|
327
|
+
state.queueSeen = true
|
|
328
|
+
return { queue: true }
|
|
329
|
+
}
|
|
330
|
+
return {}
|
|
331
|
+
}
|
|
332
|
+
if (event === 'done') {
|
|
333
|
+
state.done = true
|
|
334
|
+
const info = obj.user_message_context?.model_info
|
|
335
|
+
if (!state.actualModel && typeof info?.config_name === 'string' && info.config_name) {
|
|
336
|
+
state.actualModel = info.config_name
|
|
337
|
+
}
|
|
338
|
+
const tail = summaryTail()
|
|
339
|
+
return tail ? { text: tail, finish: 'stop' } : { finish: 'stop' }
|
|
340
|
+
}
|
|
341
|
+
if (event !== 'plan_item') return {}
|
|
342
|
+
const out = {}
|
|
343
|
+
const tci = obj.tool_call_info
|
|
344
|
+
if (tci && typeof tci === 'object' && tci.name === 'finish') {
|
|
345
|
+
const summary = tci.params?.summary
|
|
346
|
+
if (typeof summary === 'string' && summary) state.finalSummary = summary
|
|
347
|
+
}
|
|
348
|
+
const id = typeof obj.id === 'string' && obj.id ? obj.id : '(anon)'
|
|
349
|
+
if (!state.slots.has(id)) {
|
|
350
|
+
state.slots.set(id, { thought: '', reasoning: '' })
|
|
351
|
+
state.order.push(id)
|
|
352
|
+
}
|
|
353
|
+
const slot = state.slots.get(id)
|
|
354
|
+
const thoughtSnap = typeof obj.thought === 'string' ? obj.thought : ''
|
|
355
|
+
const reasoningSnap = typeof obj.reasoning_content === 'string' ? obj.reasoning_content : ''
|
|
356
|
+
const td = cumulativeDelta(slot.thought, thoughtSnap)
|
|
357
|
+
const rd = cumulativeDelta(slot.reasoning, reasoningSnap)
|
|
358
|
+
if (thoughtSnap) slot.thought = thoughtSnap
|
|
359
|
+
if (reasoningSnap) slot.reasoning = reasoningSnap
|
|
360
|
+
if (td) out.text = td
|
|
361
|
+
if (rd) out.reasoning = rd
|
|
362
|
+
return out
|
|
363
|
+
},
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cache-hit reproducer: two fresh dsh sessions get the SAME user prompt
|
|
4
|
+
* verbatim (plus one second turn in the first), all pinned to the same model
|
|
5
|
+
* (deepseek-v4-pro) via session.selectModel. The bridge dump/log then shows
|
|
6
|
+
* exactly which prefix tokens survive between identical prompts.
|
|
7
|
+
*
|
|
8
|
+
* Requires: dsh web on :3080 with CODEBUDDY_BRIDGE_LOG + CODEBUDDY_BRIDGE_DUMP.
|
|
9
|
+
* Usage: node scripts/capture-cache.mjs [port]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const PORT = process.argv[2] ?? '3080'
|
|
13
|
+
const BASE = `http://127.0.0.1:${PORT}/api/`
|
|
14
|
+
const MODEL = { provider: 'codebuddy', model: 'deepseek-v4-pro' }
|
|
15
|
+
|
|
16
|
+
async function rpc(method, payload) {
|
|
17
|
+
const res = await fetch(BASE + method, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
body: JSON.stringify({ type: 'client-request', rpcId: crypto.randomUUID(), method, payload }),
|
|
21
|
+
})
|
|
22
|
+
const body = await res.json().catch(() => null)
|
|
23
|
+
if (!body?.result?.ok) throw new Error(`${method} failed: ${JSON.stringify(body).slice(0, 200)}`)
|
|
24
|
+
return body.result.value
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
28
|
+
|
|
29
|
+
async function waitIdle(sessionId, timeoutMs = 180000) {
|
|
30
|
+
const deadline = Date.now() + timeoutMs
|
|
31
|
+
await sleep(1500)
|
|
32
|
+
while (Date.now() < deadline) {
|
|
33
|
+
const { items } = await rpc('session.list', {})
|
|
34
|
+
const item = items.find((i) => i.sessionId === sessionId)
|
|
35
|
+
if (item && item.running === false) return item
|
|
36
|
+
await sleep(1000)
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`session ${sessionId} still running after ${timeoutMs}ms`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function prompt(sessionId, text) {
|
|
42
|
+
await rpc('session.prompt', { sessionId, mode: 'queue', content: [{ type: 'text', text }] })
|
|
43
|
+
const item = await waitIdle(sessionId)
|
|
44
|
+
console.log(` done: turns=${item.projections.values.sessionStats.turns} steps=${item.projections.values.sessionStats.steps}`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const Q1 = '请用一句话介绍深井水循环系统的维护要点。'
|
|
48
|
+
const Q2 = '再补充一句冬季防冻注意事项。'
|
|
49
|
+
|
|
50
|
+
console.log('[D] session D, model=v4-pro, prompt Q1 then Q2')
|
|
51
|
+
const d = await rpc('session.create', { cwd: process.cwd() })
|
|
52
|
+
console.log(' session D:', d.sessionId)
|
|
53
|
+
await rpc('session.selectModel', { sessionId: d.sessionId, ...MODEL })
|
|
54
|
+
await prompt(d.sessionId, Q1)
|
|
55
|
+
await sleep(3500) // let the title call settle
|
|
56
|
+
await prompt(d.sessionId, Q2)
|
|
57
|
+
|
|
58
|
+
console.log('[E] fresh session E, same model, Q1 verbatim (identical resend)')
|
|
59
|
+
const e = await rpc('session.create', { cwd: process.cwd() })
|
|
60
|
+
console.log(' session E:', e.sessionId)
|
|
61
|
+
await rpc('session.selectModel', { sessionId: e.sessionId, ...MODEL })
|
|
62
|
+
await prompt(e.sessionId, Q1)
|
|
63
|
+
await sleep(3500)
|
|
64
|
+
|
|
65
|
+
console.log('\ncapture done — analyze /tmp/bridge-cache.jsonl and /tmp/bridge-dump/')
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Traffic driver for bridge forensics: talks to a RUNNING `dsh web` over its
|
|
4
|
+
* loopback RPC API and produces a controlled main-chat request sequence, so
|
|
5
|
+
* the bridge log (CODEBUDDY_BRIDGE_LOG) can be classified afterwards:
|
|
6
|
+
*
|
|
7
|
+
* session A: three short turns → full-history resend growth + title call
|
|
8
|
+
* session B: A's first prompt verbatim → "manual resend" instance
|
|
9
|
+
* session C: asks for a subagent → parent/child request streams
|
|
10
|
+
*
|
|
11
|
+
* Real gateway cost: ~8-12 tiny completions (answers are short by prompt
|
|
12
|
+
* design; title calls cap at 64 tokens). Requires: dsh web on 127.0.0.1:3080
|
|
13
|
+
* with this plugin loaded and the bridge log enabled.
|
|
14
|
+
*
|
|
15
|
+
* Usage: node scripts/capture-traffic.mjs [port]
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const PORT = process.argv[2] ?? '3080'
|
|
19
|
+
const BASE = `http://127.0.0.1:${PORT}/api/`
|
|
20
|
+
|
|
21
|
+
async function rpc(method, payload) {
|
|
22
|
+
const res = await fetch(BASE + method, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25
|
+
body: JSON.stringify({ type: 'client-request', rpcId: crypto.randomUUID(), method, payload }),
|
|
26
|
+
})
|
|
27
|
+
const body = await res.json().catch(() => null)
|
|
28
|
+
if (!body?.result?.ok) throw new Error(`${method} failed: ${JSON.stringify(body).slice(0, 200)}`)
|
|
29
|
+
return body.result.value
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
33
|
+
|
|
34
|
+
/** Poll session.list until the session reports running=false. */
|
|
35
|
+
async function waitIdle(sessionId, timeoutMs = 120000) {
|
|
36
|
+
const deadline = Date.now() + timeoutMs
|
|
37
|
+
await sleep(1500) // let the turn register first
|
|
38
|
+
while (Date.now() < deadline) {
|
|
39
|
+
const { items } = await rpc('session.list', {})
|
|
40
|
+
const item = items.find((i) => i.sessionId === sessionId)
|
|
41
|
+
if (item && item.running === false) return item
|
|
42
|
+
await sleep(1000)
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`session ${sessionId} still running after ${timeoutMs}ms`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function prompt(sessionId, text) {
|
|
48
|
+
await rpc('session.prompt', {
|
|
49
|
+
sessionId,
|
|
50
|
+
mode: 'queue',
|
|
51
|
+
content: [{ type: 'text', text }],
|
|
52
|
+
})
|
|
53
|
+
const item = await waitIdle(sessionId)
|
|
54
|
+
const usage = item?.projections?.values?.tokenUsage ?? null
|
|
55
|
+
console.log(` done: turns=${item.projections.values.sessionStats.turns} steps=${item.projections.values.sessionStats.steps}`
|
|
56
|
+
+ ` tokenUsage=${JSON.stringify(usage)}`)
|
|
57
|
+
return item
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const Q1 = '只回复两个字:好的'
|
|
61
|
+
const Q2 = '再只回复两个字:收到'
|
|
62
|
+
const Q3 = '最后只回复两个字:完成'
|
|
63
|
+
|
|
64
|
+
console.log('[A] session with three short turns')
|
|
65
|
+
const a = await rpc('session.create', { cwd: process.cwd() })
|
|
66
|
+
console.log(' session A:', a.sessionId)
|
|
67
|
+
await prompt(a.sessionId, Q1)
|
|
68
|
+
await sleep(3000) // the title call fires after the first turn settles
|
|
69
|
+
await prompt(a.sessionId, Q2)
|
|
70
|
+
await prompt(a.sessionId, Q3)
|
|
71
|
+
|
|
72
|
+
console.log('[B] new session, A’s first prompt verbatim (manual resend)')
|
|
73
|
+
const b = await rpc('session.create', { cwd: process.cwd() })
|
|
74
|
+
console.log(' session B:', b.sessionId)
|
|
75
|
+
await prompt(b.sessionId, Q1)
|
|
76
|
+
await sleep(3000)
|
|
77
|
+
|
|
78
|
+
console.log('[C] new session, subagent attempt')
|
|
79
|
+
const c = await rpc('session.create', { cwd: process.cwd() })
|
|
80
|
+
console.log(' session C:', c.sessionId)
|
|
81
|
+
await prompt(c.sessionId, '请使用 subagent 工具启动一个子代理,让它回答"1+1等于几"并报告结果。你自己不要回答。')
|
|
82
|
+
|
|
83
|
+
console.log('\ncapture done — now classify the bridge log (CODEBUDDY_BRIDGE_LOG).')
|