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,399 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* probe-trae-agent-v3.mjs — Trae agent 面(llm_utils_chat 的 agent_v3 function 家族)
|
|
4
|
+
* 真实凭据探针(M1:证据先于代码)。
|
|
5
|
+
*
|
|
6
|
+
* 背景(docs/diagnosis-trae-3003.md §12 + 本 goal 立项):inline_chat 面 3003 已
|
|
7
|
+
* 6 周未愈(scene 级故障),remote 面(chat_sessions)架构性拒绝 OpenAI tools
|
|
8
|
+
* (remote-no-tools)。社区两个独立实现(dsh-connect-trae / Trae2api-cn)主张
|
|
9
|
+
* llm_utils_chat 存在「agent 用法」:接受 OpenAI 风格 tools、返回结构化
|
|
10
|
+
* tool_calls、工具由调用端本地执行。按踩坑 #22,一切以本探针实测为准。
|
|
11
|
+
*
|
|
12
|
+
* 臂位(≥6,expect 预注册;--arms 子集跑):
|
|
13
|
+
* A1 纯聊天(无 tools) —— 决策门臂 1
|
|
14
|
+
* A2 tools + tool_choice=auto(提示触发工具)—— 决策门臂 2 前置
|
|
15
|
+
* A3 两轮工具闭环(assistant tool_calls + role:tool 回传 → 最终回答)—— 决策门臂 2
|
|
16
|
+
* A4 并行调用提示(期望 0..N 个 tool_calls,如实记录)
|
|
17
|
+
* A5 reasoning_effort 字段注入(对照 A2 同工具;记录是否被忽略/拒绝)
|
|
18
|
+
* E1 错模型名(不存在的模型 id)
|
|
19
|
+
* E2 坏参数形态(temperature 字符串)
|
|
20
|
+
* Q 额度差分观测(payload 级提示,默认跳过):A1 前后各读一次
|
|
21
|
+
* ide_user_ent_usage,比对 IDE 池 used 增量(<0.01 分辨率不可见则如实记录)
|
|
22
|
+
*
|
|
23
|
+
* 用法:
|
|
24
|
+
* node scripts/probe-trae-agent-v3.mjs [--fn agent_chat] [--model glm-5.3]
|
|
25
|
+
* [--arms A1,A2,A3] [--sweep] [--quota] [--raw]
|
|
26
|
+
* --sweep:只跑 FN_SWEEP 候选 function 名扫描(每候选一发 A1 形态,间隔 3s),
|
|
27
|
+
* 用于定位「agent 用法」的真实 function 值(2001=no function config 即否决)。
|
|
28
|
+
* 决策门(goal 明文):A1 与 A3 任一失败(含 3003)→ 停止实装,goal 以前提被证伪
|
|
29
|
+
* 收束。脚本报答案在末尾 GATE 行给出。
|
|
30
|
+
*
|
|
31
|
+
* 纪律:臂间隔 ≥2s(raw 面历史 4011 限流阈 20s 的保守化:本探针臂数少、并发恒 1);
|
|
32
|
+
* 单账号(~/.dsh/trae-plugin-auth.json);写操作为零(除 Q 臂只读额度接口);
|
|
33
|
+
* 证据落 docs/probes/trae-agent-v3-<date>.jsonl;token 永不落盘(JSONL 无凭据字段)。
|
|
34
|
+
*/
|
|
35
|
+
import { writeFileSync, mkdirSync } from 'node:fs'
|
|
36
|
+
import { homedir } from 'node:os'
|
|
37
|
+
import { join, dirname } from 'node:path'
|
|
38
|
+
|
|
39
|
+
import { createTraeOAuth } from '../providers/trae/oauth.js'
|
|
40
|
+
import { buildChatRequest, traeOutboundHeaders } from '../providers/trae/gateway.js'
|
|
41
|
+
import { readJson } from '../core/json-store.js'
|
|
42
|
+
|
|
43
|
+
const args = process.argv.slice(2)
|
|
44
|
+
const argOf = (n, d) => { const i = args.indexOf(n); return i >= 0 && args[i + 1] !== undefined ? args[i + 1] : d }
|
|
45
|
+
const FN = argOf('--fn', 'solo_work_lite')
|
|
46
|
+
const MODEL = argOf('--model', 'glm-5.3')
|
|
47
|
+
const ARMS = new Set(argOf('--arms', 'A1,A2,A3,A4,A5,E1,E2').split(',').map((s) => s.trim()))
|
|
48
|
+
const WITH_QUOTA = args.includes('--quota')
|
|
49
|
+
const DUMP_RAW = args.includes('--raw')
|
|
50
|
+
const SWEEP = args.includes('--sweep')
|
|
51
|
+
const SWEEP2 = args.includes('--sweep2')
|
|
52
|
+
const SWEEP_MODELS = args.includes('--sweep-models')
|
|
53
|
+
// function 位候选扫描清单(--sweep):2001=no function config 即否决该名字。
|
|
54
|
+
// 依据:历史实测 inline_chat/chat_v3/solo_agent_lite/solo_work_lite 均注册
|
|
55
|
+
// (docs/reverse/trae-cloud-api.md §5.1);社区实现主张的 agent 用法 function 名
|
|
56
|
+
// 未公开钉死(踩坑 #22:以实测为准)。
|
|
57
|
+
const FN_SWEEP = ['agent_chat', 'agent_v3', 'solo_agent_chat', 'agent', 'chat', 'solo_agent', 'assistant_chat']
|
|
58
|
+
// sweep 第二轮(--sweep2):chat 是注册 function(4023=模型未知而非 2001=function 未注册),
|
|
59
|
+
// 对它补目录真实模型名 + tools 两发,判定「chat 即 agent 用法」假设。
|
|
60
|
+
const FN_SWEEP2 = ['chat', 'chat_v3']
|
|
61
|
+
|
|
62
|
+
const DSH_HOME = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
63
|
+
const AUTH_PATH = join(DSH_HOME, 'trae-plugin-auth.json')
|
|
64
|
+
const MCHOST = 'https://trae-api-cn.mchost.guru'
|
|
65
|
+
const SETTINGS = { traeAuthBaseURL: 'https://api.trae.cn', traeChatBaseURL: MCHOST, traeLoginHost: 'https://www.trae.cn' }
|
|
66
|
+
|
|
67
|
+
const oauth = createTraeOAuth({ readAuth: () => readJson(AUTH_PATH), writeAuth: () => { /* 探针不写回 */ } })
|
|
68
|
+
const cred = await oauth.resolveTraeCredential(SETTINGS)
|
|
69
|
+
if (!cred) { console.error('未登录 Trae(先跑 node scripts/probe-trae-live.mjs --login)'); process.exit(2) }
|
|
70
|
+
const token = String(cred.authorization).replace(/^Cloud-IDE-JWT\s+/, '')
|
|
71
|
+
const authStore = readJson(AUTH_PATH)
|
|
72
|
+
const device = authStore.device ?? {}
|
|
73
|
+
const uid = String(authStore.account?.uid ?? '')
|
|
74
|
+
const ideHeaders = (requestId) => ({
|
|
75
|
+
...traeOutboundHeaders(device, uid ? Number(uid) : undefined, requestId),
|
|
76
|
+
Authorization: `Cloud-IDE-JWT ${token}`,
|
|
77
|
+
'X-Cloudide-Token': token,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// SSE 帧摘要(原文可选 --raw 落盘;默认只落结构化摘要,token 与长文本不进证据)
|
|
82
|
+
|
|
83
|
+
function summarizeSse(raw) {
|
|
84
|
+
const events = []
|
|
85
|
+
let lastEvt = null
|
|
86
|
+
for (const l of raw.split('\n')) {
|
|
87
|
+
const t = l.trim()
|
|
88
|
+
if (!t) continue
|
|
89
|
+
if (t.startsWith('event:')) { lastEvt = t.slice(6).trim(); continue }
|
|
90
|
+
if (!t.startsWith('data:')) continue
|
|
91
|
+
const data = t.slice(5).trim()
|
|
92
|
+
if (data === '[DONE]') { events.push({ ev: lastEvt ?? null, done: true }); continue }
|
|
93
|
+
let j
|
|
94
|
+
try { j = JSON.parse(data) } catch { events.push({ ev: lastEvt ?? null, nonJson: data.slice(0, 80) }); continue }
|
|
95
|
+
const ev = (typeof j.event === 'string' && j.event) ? j.event : lastEvt
|
|
96
|
+
const rec = { ev: ev ?? null }
|
|
97
|
+
if (j.code != null) rec.code = j.code
|
|
98
|
+
if (typeof j.message === 'string') rec.message = j.message.slice(0, 200)
|
|
99
|
+
if (typeof j.provider_model_name === 'string') rec.providerModel = j.provider_model_name
|
|
100
|
+
if (typeof j.response === 'string') rec.responseLen = j.response.length
|
|
101
|
+
if (typeof j.reasoning_content === 'string') rec.reasoningLen = j.reasoning_content.length
|
|
102
|
+
if (Array.isArray(j.tool_calls)) rec.toolCalls = j.tool_calls.map((c) => ({
|
|
103
|
+
index: c.index, id: c.id, hasFnCall: Boolean(c.function_call), hasFn: Boolean(c.function),
|
|
104
|
+
name: c.function_call?.name ?? c.function?.name,
|
|
105
|
+
argsLen: String(c.function_call?.arguments ?? c.function?.arguments ?? '').length,
|
|
106
|
+
}))
|
|
107
|
+
if (j.tool_call_info && typeof j.tool_call_info === 'object') {
|
|
108
|
+
rec.toolCallInfo = { name: j.tool_call_info.name, id: j.tool_call_info.tool_call_id ?? j.tool_call_info.id }
|
|
109
|
+
}
|
|
110
|
+
if (j.finish_reason) rec.finish = j.finish_reason
|
|
111
|
+
// token_usage 事件形态(dsh-connect-trae sse.ts 全字段表):顶层即计数
|
|
112
|
+
if (ev === 'token_usage') {
|
|
113
|
+
rec.usage = {
|
|
114
|
+
prompt_tokens: j.prompt_tokens, completion_tokens: j.completion_tokens,
|
|
115
|
+
total_tokens: j.total_tokens, reasoning_tokens: j.reasoning_tokens,
|
|
116
|
+
cache_read_input_tokens: j.cache_read_input_tokens,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
events.push(rec)
|
|
120
|
+
}
|
|
121
|
+
return events
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 从摘要重建可见文本(response 为累计快照时取最后一帧)。 */
|
|
125
|
+
function finalTextOf(events) {
|
|
126
|
+
let t = ''
|
|
127
|
+
for (const e of events) if (typeof e.responseLen === 'number') t = e // 只取长度不足以重建文本;真值由 --raw 提供
|
|
128
|
+
return null // 摘要模式不重建文本(文本含模型输出,证据只记长度与事件形态)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
134
|
+
|
|
135
|
+
async function postLlmUtilsChat(payload, { fn, fnKey, timeoutMs = 120_000 } = {}) {
|
|
136
|
+
const { body, requestId } = buildChatRequest(payload, `agentv3-${Date.now().toString(36)}`, { fnKey })
|
|
137
|
+
body.function = fn
|
|
138
|
+
const t0 = Date.now()
|
|
139
|
+
const resp = await fetch(`${MCHOST}/api/agent/v3/llm_utils_chat`, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: ideHeaders(requestId),
|
|
142
|
+
body: JSON.stringify(body),
|
|
143
|
+
redirect: 'follow',
|
|
144
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
145
|
+
})
|
|
146
|
+
const raw = await resp.text()
|
|
147
|
+
return { status: resp.status, raw, ms: Date.now() - t0, sentBody: body }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const TOOL_GET_TIME = {
|
|
151
|
+
type: 'function',
|
|
152
|
+
function: {
|
|
153
|
+
name: 'get_current_time',
|
|
154
|
+
description: '获取当前时间。当用户询问时间、日期时必须调用。',
|
|
155
|
+
parameters: { type: 'object', properties: {} },
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// expect 预注册(跑前钉死,跑后对照)
|
|
160
|
+
const EXPECT = {
|
|
161
|
+
A1: '若 function 有效:HTTP 200 + SSE 文本事件 + done;若 fn 未知:响亮错误(码入证据)',
|
|
162
|
+
A2: '接受 tools 不 400;若支持工具则出 tool_calls 事件或文本回答',
|
|
163
|
+
A3: '第二轮(带 role:tool)不报错且产出最终文本回答',
|
|
164
|
+
A4: '并行提示:0..N 个 tool_calls(如实记录,N≥2 即证实并行)',
|
|
165
|
+
A5: 'reasoning_effort 被接受(不报错)或被忽略(行为同 A2);若 400 则证实该面拒此字段',
|
|
166
|
+
E1: '错模型名:业务错误码(预期 3003/4001/4011 家族之一)',
|
|
167
|
+
E2: '坏参数:HTTP 4xx 或 SSE error(预期 4001 unmarshal 家族)',
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const arms = []
|
|
171
|
+
|
|
172
|
+
async function runArm(label, fn, payload, { expect, fnKey, timeoutMs } = {}) {
|
|
173
|
+
const out = { arm: label, fn, expect, at: new Date().toISOString() }
|
|
174
|
+
try {
|
|
175
|
+
const r = await postLlmUtilsChat(payload, { fn, fnKey, timeoutMs })
|
|
176
|
+
out.http = r.status
|
|
177
|
+
out.ms = r.ms
|
|
178
|
+
out.sentBodyShape = {
|
|
179
|
+
model: r.sentBody.model, function: r.sentBody.function,
|
|
180
|
+
messages: r.sentBody.messages.length,
|
|
181
|
+
roles: r.sentBody.messages.map((m) => m.role),
|
|
182
|
+
assistantToolCallKey: r.sentBody.messages.find((m) => m.role === 'assistant')?.tool_calls?.[0]
|
|
183
|
+
? Object.keys(r.sentBody.messages.find((m) => m.role === 'assistant').tool_calls[0]).join('+') : null,
|
|
184
|
+
tools: Array.isArray(r.sentBody.tools) ? r.sentBody.tools.length : 0,
|
|
185
|
+
tool_choice: r.sentBody.tool_choice ?? null,
|
|
186
|
+
reasoning_effort: r.sentBody.reasoning_effort ?? null,
|
|
187
|
+
}
|
|
188
|
+
const events = summarizeSse(r.raw)
|
|
189
|
+
out.sseBytes = r.raw.length
|
|
190
|
+
out.eventCount = events.length
|
|
191
|
+
out.errors = events.filter((e) => e.code != null).slice(0, 3)
|
|
192
|
+
out.finishReasons = [...new Set(events.map((e) => e.finish ?? e.stopReason).filter(Boolean))]
|
|
193
|
+
out.providerModels = [...new Set(events.map((e) => e.providerModel).filter(Boolean))]
|
|
194
|
+
out.toolCallEvents = events.filter((e) => e.toolCalls || e.toolCallInfo).slice(0, 6)
|
|
195
|
+
out.maxResponseLen = Math.max(0, ...events.map((e) => e.responseLen ?? 0))
|
|
196
|
+
out.usage = events.map((e) => e.usage).filter(Boolean).at(-1) ?? null
|
|
197
|
+
out.eventNames = [...new Set(events.map((e) => e.ev).filter(Boolean))]
|
|
198
|
+
if (DUMP_RAW) out.rawSse = r.raw.slice(0, 20_000)
|
|
199
|
+
out.ok = out.http === 200 && out.errors.length === 0
|
|
200
|
+
} catch (err) {
|
|
201
|
+
out.ok = false
|
|
202
|
+
out.error = String(err?.message ?? err).slice(0, 300)
|
|
203
|
+
}
|
|
204
|
+
arms.push(out)
|
|
205
|
+
console.log(`[${label}] http=${out.http ?? '-'} ok=${out.ok} events=${out.eventCount ?? 0} errors=${JSON.stringify(out.errors ?? out.error ?? null)}`)
|
|
206
|
+
return out
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
console.log(`fn=${FN} model=${MODEL} arms=${[...ARMS].join(',')}${SWEEP ? ' SWEEP=' + FN_SWEEP.join('/') : ''}`)
|
|
212
|
+
console.log('expect(预注册):', JSON.stringify(EXPECT))
|
|
213
|
+
|
|
214
|
+
if (SWEEP) {
|
|
215
|
+
for (const fn of FN_SWEEP) {
|
|
216
|
+
await runArm(`sweep:${fn}`, fn, {
|
|
217
|
+
model: MODEL, stream: true,
|
|
218
|
+
messages: [{ role: 'user', content: '只回复两个字:成功' }],
|
|
219
|
+
}, { expect: 'function 名注册判定:2001=no function config 即否决', timeoutMs: 60_000 })
|
|
220
|
+
await sleep(3_000)
|
|
221
|
+
}
|
|
222
|
+
const date0 = new Date().toISOString().slice(0, 10)
|
|
223
|
+
const sweepPath = join(process.cwd(), 'docs/probes', `trae-agent-v3-fnsweep-${date0}.jsonl`)
|
|
224
|
+
mkdirSync(dirname(sweepPath), { recursive: true })
|
|
225
|
+
writeFileSync(sweepPath, arms.map((a) => JSON.stringify(a)).join('\n') + '\n')
|
|
226
|
+
console.log(`证据 → ${sweepPath}`)
|
|
227
|
+
process.exit(0)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let quotaBefore = null
|
|
231
|
+
async function quotaSnapshot(tag) {
|
|
232
|
+
const resp = await fetch(`${SETTINGS.traeAuthBaseURL}/trae/api/v2/pay/ide_user_ent_usage`, {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: ideHeaders(`quota-${tag}`),
|
|
235
|
+
body: JSON.stringify({ require_usage: true, req_source: 0 }),
|
|
236
|
+
signal: AbortSignal.timeout(15_000),
|
|
237
|
+
})
|
|
238
|
+
const body = await resp.json().catch(() => null)
|
|
239
|
+
const packs = body?.user_entitlement_pack_list
|
|
240
|
+
if (!Array.isArray(packs)) return { tag, error: `code ${body?.code ?? `http ${resp.status}`}` }
|
|
241
|
+
const mk = () => ({ limit: 0, used: 0 })
|
|
242
|
+
const pools = { ide: mk(), work: mk() }
|
|
243
|
+
for (const p of packs) {
|
|
244
|
+
const ep = p?.entitlement_base_info?.available_endpoint
|
|
245
|
+
const pool = ep === 1 ? pools.work : ep === 0 ? pools.ide : null
|
|
246
|
+
if (!pool) continue
|
|
247
|
+
pool.limit += p?.entitlement_base_info?.quota?.credits_limit ?? 0
|
|
248
|
+
pool.used += p?.usage?.credits_amount ?? 0
|
|
249
|
+
}
|
|
250
|
+
const r = (n) => Math.round(n * 1000) / 1000
|
|
251
|
+
return { tag, ideUsed: r(pools.ide.used), ideLimit: pools.ide.limit, workUsed: r(pools.work.used), workLimit: pools.work.limit }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (WITH_QUOTA) quotaBefore = await quotaSnapshot('before')
|
|
255
|
+
|
|
256
|
+
if (SWEEP2) {
|
|
257
|
+
for (const fn of FN_SWEEP2) {
|
|
258
|
+
await runArm(`sweep2:${fn}:plain`, fn, {
|
|
259
|
+
model: MODEL, stream: true,
|
|
260
|
+
messages: [{ role: 'user', content: '只回复两个字:成功' }],
|
|
261
|
+
}, { expect: '已注册 function + 真实模型名:期望 200 出文本', timeoutMs: 90_000 })
|
|
262
|
+
await sleep(3_000)
|
|
263
|
+
await runArm(`sweep2:${fn}:tools`, fn, {
|
|
264
|
+
model: MODEL, stream: true, tool_choice: 'auto',
|
|
265
|
+
messages: [{ role: 'user', content: '现在几点?请调用 get_current_time 工具获取。' }],
|
|
266
|
+
tools: [TOOL_GET_TIME],
|
|
267
|
+
}, { expect: '带 tools:接受且出 tool_calls 即证实工具面', timeoutMs: 90_000 })
|
|
268
|
+
await sleep(3_000)
|
|
269
|
+
}
|
|
270
|
+
const date0 = new Date().toISOString().slice(0, 10)
|
|
271
|
+
const p = join(process.cwd(), 'docs/probes', `trae-agent-v3-fnsweep2-${date0}.jsonl`)
|
|
272
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
273
|
+
writeFileSync(p, arms.map((a) => JSON.stringify(a)).join('\n') + '\n')
|
|
274
|
+
console.log(`证据 → ${p}`)
|
|
275
|
+
process.exit(0)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (ARMS.has('A1')) {
|
|
279
|
+
await runArm('A1', FN, {
|
|
280
|
+
model: MODEL, stream: true,
|
|
281
|
+
messages: [{ role: 'user', content: '只回复两个字:成功' }],
|
|
282
|
+
}, { expect: EXPECT.A1 })
|
|
283
|
+
await sleep(2_000)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let a2 = null
|
|
287
|
+
if (ARMS.has('A2')) {
|
|
288
|
+
a2 = await runArm('A2', FN, {
|
|
289
|
+
model: MODEL, stream: true, tool_choice: 'auto',
|
|
290
|
+
messages: [{ role: 'user', content: '现在几点?请调用 get_current_time 工具获取。' }],
|
|
291
|
+
tools: [TOOL_GET_TIME],
|
|
292
|
+
}, { expect: EXPECT.A2 })
|
|
293
|
+
await sleep(2_000)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (ARMS.has('A3')) {
|
|
297
|
+
// 两轮闭环(Trae2api-cn 参照形态):assistant 历史消息的 tool_calls 用
|
|
298
|
+
// **function_call 键**(与该面 SSE 出站的 tool_calls[i].function_call 同构——
|
|
299
|
+
// 2026-10-05 A3 臂实测:OpenAI 风格 function 键被 proto 层拒绝「required
|
|
300
|
+
// field Name is not set」,说明该面把 function_call.Name 映射到 proto 字段)。
|
|
301
|
+
// tool_calls 形态以 A2 实测为准(A2 若未出 tool_calls,则构造等价形态——
|
|
302
|
+
// 该臂目的是验证「role:tool 回传被接受且产出最终回答」这一协议面)。
|
|
303
|
+
const callId = a2?.toolCallEvents?.[0]?.toolCalls?.[0]?.id
|
|
304
|
+
?? a2?.toolCallEvents?.[0]?.toolCallInfo?.id
|
|
305
|
+
?? 'call_probe_time_1'
|
|
306
|
+
const callName = a2?.toolCallEvents?.[0]?.toolCalls?.[0]?.name
|
|
307
|
+
?? a2?.toolCallEvents?.[0]?.toolCallInfo?.name
|
|
308
|
+
?? 'get_current_time'
|
|
309
|
+
await runArm('A3', FN, {
|
|
310
|
+
model: MODEL, stream: true, tool_choice: 'auto',
|
|
311
|
+
messages: [
|
|
312
|
+
{ role: 'user', content: '现在几点?请调用 get_current_time 工具获取。' },
|
|
313
|
+
{ role: 'assistant', content: '', tool_calls: [{ id: callId, type: 'function', function: { name: callName, arguments: '{}' } }] },
|
|
314
|
+
{ role: 'tool', tool_call_id: callId, name: callName, content: '2026-10-05 14:30:00 CST' },
|
|
315
|
+
],
|
|
316
|
+
tools: [TOOL_GET_TIME],
|
|
317
|
+
}, { expect: EXPECT.A3, fnKey: 'function_call' })
|
|
318
|
+
await sleep(2_000)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (ARMS.has('A4')) {
|
|
322
|
+
await runArm('A4', FN, {
|
|
323
|
+
model: MODEL, stream: true, tool_choice: 'auto',
|
|
324
|
+
messages: [{ role: 'user', content: '请同时调用两次 get_current_time(并行),然后告诉我时间。' }],
|
|
325
|
+
tools: [TOOL_GET_TIME],
|
|
326
|
+
}, { expect: EXPECT.A4 })
|
|
327
|
+
await sleep(2_000)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (ARMS.has('A5')) {
|
|
331
|
+
await runArm('A5', FN, {
|
|
332
|
+
model: MODEL, stream: true, tool_choice: 'auto', reasoning_effort: 'high',
|
|
333
|
+
messages: [{ role: 'user', content: '现在几点?请调用 get_current_time 工具获取。' }],
|
|
334
|
+
tools: [TOOL_GET_TIME],
|
|
335
|
+
}, { expect: EXPECT.A5 })
|
|
336
|
+
await sleep(2_000)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (ARMS.has('E1')) {
|
|
340
|
+
await runArm('E1', FN, {
|
|
341
|
+
model: 'no-such-model-agentv3-probe', stream: true,
|
|
342
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
343
|
+
}, { expect: EXPECT.E1 })
|
|
344
|
+
await sleep(2_000)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (ARMS.has('E2')) {
|
|
348
|
+
await runArm('E2', FN, {
|
|
349
|
+
model: MODEL, stream: true, temperature: 'hot',
|
|
350
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
351
|
+
}, { expect: EXPECT.E2 })
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let quotaAfter = null
|
|
355
|
+
if (WITH_QUOTA) { await sleep(2_000); quotaAfter = await quotaSnapshot('after') }
|
|
356
|
+
|
|
357
|
+
// M1 轮次 A6:多模型对照(单发/模型,证明 agent 面模型路由非钉死)。
|
|
358
|
+
// 历史事实:raw 面「模型路由被 function 位钉死」(trae-cloud-api.md §5.1,
|
|
359
|
+
// solo_work_lite 恒 glm-5.2);timing_cost.provider_model_name 是唯一真值源。
|
|
360
|
+
if (ARMS.has('A6')) {
|
|
361
|
+
for (const m of ['kimi-k2.6', 'DeepSeek-V4-Flash']) {
|
|
362
|
+
await runArm(`A6:${m}`, FN, {
|
|
363
|
+
model: m, stream: true,
|
|
364
|
+
messages: [{ role: 'user', content: '只回复两个字:成功' }],
|
|
365
|
+
}, { expect: 'provider_model_name 跟随请求模型即证实真路由;钉死则恒 glm-5.2' })
|
|
366
|
+
await sleep(2_000)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
const gate = {
|
|
373
|
+
A1: arms.find((a) => a.arm === 'A1'),
|
|
374
|
+
A3: arms.find((a) => a.arm === 'A3'),
|
|
375
|
+
}
|
|
376
|
+
const gatePass = (gate.A1 ? gate.A1.ok : null) && (gate.A3 ? gate.A3.ok : null)
|
|
377
|
+
|
|
378
|
+
const evidence = {
|
|
379
|
+
probe: 'trae-agent-v3',
|
|
380
|
+
at: new Date().toISOString(),
|
|
381
|
+
fn: FN,
|
|
382
|
+
model: MODEL,
|
|
383
|
+
accountUidMasked: uid.slice(0, 3) + '***',
|
|
384
|
+
expect: EXPECT,
|
|
385
|
+
arms,
|
|
386
|
+
quota: WITH_QUOTA ? {
|
|
387
|
+
before: quotaBefore, after: quotaAfter,
|
|
388
|
+
deltaIde: quotaBefore?.ideUsed != null && quotaAfter?.ideUsed != null ? Math.round((quotaAfter.ideUsed - quotaBefore.ideUsed) * 1000) / 1000 : null,
|
|
389
|
+
deltaWork: quotaBefore?.workUsed != null && quotaAfter?.workUsed != null ? Math.round((quotaAfter.workUsed - quotaBefore.workUsed) * 1000) / 1000 : null,
|
|
390
|
+
} : undefined,
|
|
391
|
+
gate: gatePass == null ? 'SKIPPED(A1/A3 未跑)' : gatePass ? 'PASS(A1 与 A3 均成功)' : 'FAIL(决策门臂失败,停止实装)',
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
395
|
+
const outPath = join(process.cwd(), 'docs/probes', `trae-agent-v3-${date}.jsonl`)
|
|
396
|
+
mkdirSync(dirname(outPath), { recursive: true })
|
|
397
|
+
writeFileSync(outPath, arms.map((a) => JSON.stringify(a)).join('\n') + '\n' + JSON.stringify({ summary: evidence.gate, quota: evidence.quota ?? null }) + '\n')
|
|
398
|
+
console.log(`GATE: ${evidence.gate}`)
|
|
399
|
+
console.log(`证据 → ${outPath}`)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* probe-trae-efforts.mjs — Trae 通道 `reasoning_effort` 方言判定(真实上游,小额)。
|
|
4
|
+
*
|
|
5
|
+
* 为什么需要它:Trae 路由的 `compat` 一直没开(cordis.patch.yml:369-371 注明
|
|
6
|
+
* "reasoning_effort dialect unverified"),所以该通道在宿主「推理等级」里从不出档。
|
|
7
|
+
* 目录(state.vscdb / 远端模型表)其实**声明了逐模型档位**
|
|
8
|
+
* (`reasoning_effort_config.options`,实测 Doubao-Seed-2.1-Pro = ["light","high"],
|
|
9
|
+
* default_level "high"),只要方言可用就能同 Qoder 一样自动出档。
|
|
10
|
+
*
|
|
11
|
+
* 判据(踩坑 #42 纪律):自适应思考下推理长度是非单调弱信号 ⇒ 必须**多臂多采样**,
|
|
12
|
+
* 只有"每个重复都落在基线的同一侧"才判 DIRECTION-CONSISTENT;否则 INCONCLUSIVE,
|
|
13
|
+
* 不下结论、不改出站。
|
|
14
|
+
*
|
|
15
|
+
* 用法:
|
|
16
|
+
* node scripts/probe-trae-efforts.mjs [--model <id>] [--levels light,high]
|
|
17
|
+
* [--repeat 2] [--prompt "…"]
|
|
18
|
+
* 证据 → docs/probes/trae-efforts-<ts>.json(不含任何令牌)
|
|
19
|
+
*/
|
|
20
|
+
import { writeFileSync, mkdirSync } from 'node:fs'
|
|
21
|
+
import { homedir } from 'node:os'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
|
|
24
|
+
import { createTraeOAuth } from '../providers/trae/oauth.js'
|
|
25
|
+
import { buildChatRequest, createTraeStreamParser, TRAE_APP_ID, TRAE_IDE_VERSION, TRAE_IDE_VERSION_CODE } from '../providers/trae/gateway.js'
|
|
26
|
+
import { readJson } from '../core/json-store.js'
|
|
27
|
+
|
|
28
|
+
const DSH_HOME = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
29
|
+
const AUTH_PATH = join(DSH_HOME, 'trae-plugin-auth.json')
|
|
30
|
+
const SETTINGS = {
|
|
31
|
+
traeAuthBaseURL: process.env.TRAE_AUTH_BASE ?? 'https://api.trae.cn',
|
|
32
|
+
traeChatBaseURL: process.env.TRAE_CHAT_BASE ?? 'https://trae-api-cn.mchost.guru',
|
|
33
|
+
traeLoginHost: process.env.TRAE_LOGIN_HOST ?? 'https://www.trae.cn',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const args = process.argv.slice(2)
|
|
37
|
+
const argOf = (name, dflt) => {
|
|
38
|
+
const i = args.indexOf(name)
|
|
39
|
+
return i >= 0 && args[i + 1] !== undefined ? args[i + 1] : dflt
|
|
40
|
+
}
|
|
41
|
+
const MODEL = argOf('--model', 'Doubao-Seed-2.1-Pro')
|
|
42
|
+
const LEVELS = String(argOf('--levels', 'light,high')).split(',').map((s) => s.trim()).filter(Boolean)
|
|
43
|
+
const REPEAT = Number(argOf('--repeat', '2')) || 2
|
|
44
|
+
const PROMPT = argOf('--prompt', '一个笼子里有鸡和兔共 10 只,脚共 28 只。鸡兔各几只?只给最终数字。')
|
|
45
|
+
|
|
46
|
+
const oauth = createTraeOAuth({ readAuth: () => readJson(AUTH_PATH), writeAuth: () => {} })
|
|
47
|
+
const cred = await oauth.resolveTraeCredential(SETTINGS)
|
|
48
|
+
if (!cred) {
|
|
49
|
+
console.error('未登录 Trae(先跑 node scripts/probe-trae-live.mjs --login)')
|
|
50
|
+
process.exit(2)
|
|
51
|
+
}
|
|
52
|
+
const token = String(cred.authorization).replace(/^Cloud-IDE-JWT\s+/, '')
|
|
53
|
+
const authStore = readJson(AUTH_PATH)
|
|
54
|
+
|
|
55
|
+
async function arm(label, level) {
|
|
56
|
+
const payload = {
|
|
57
|
+
model: MODEL, stream: true,
|
|
58
|
+
messages: [{ role: 'user', content: PROMPT }],
|
|
59
|
+
...(level === undefined ? {} : { reasoning_effort: level }),
|
|
60
|
+
}
|
|
61
|
+
const { body, requestId } = buildChatRequest(payload, 'probe-efforts')
|
|
62
|
+
const res = await fetch(`${SETTINGS.traeChatBaseURL}/api/agent/v3/llm_utils_chat`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'application/json',
|
|
66
|
+
Accept: 'text/event-stream',
|
|
67
|
+
Authorization: `Cloud-IDE-JWT ${token}`,
|
|
68
|
+
'X-Cloudide-Token': token,
|
|
69
|
+
'x-ide-token': token,
|
|
70
|
+
'x-app-id': TRAE_APP_ID,
|
|
71
|
+
'x-ide-version': TRAE_IDE_VERSION,
|
|
72
|
+
'x-ide-version-code': TRAE_IDE_VERSION_CODE,
|
|
73
|
+
'x-request-id': requestId,
|
|
74
|
+
...(authStore.account?.uid ? { 'x-uid': String(authStore.account.uid) } : {}),
|
|
75
|
+
...(authStore.device?.deviceId ? { 'x-device-id': authStore.device.deviceId } : {}),
|
|
76
|
+
...(authStore.device?.machineId ? { 'x-machine-id': authStore.device.machineId } : {}),
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify(body),
|
|
79
|
+
})
|
|
80
|
+
const raw = await res.text()
|
|
81
|
+
const parser = createTraeStreamParser()
|
|
82
|
+
let reasoning = ''
|
|
83
|
+
let text = ''
|
|
84
|
+
let inband = null
|
|
85
|
+
for (const line of raw.split('\n')) {
|
|
86
|
+
const s = line.trim()
|
|
87
|
+
if (!s.startsWith('data:')) continue
|
|
88
|
+
const data = s.slice(5).trim()
|
|
89
|
+
if (!data || data === '[DONE]') continue
|
|
90
|
+
let chunk
|
|
91
|
+
try { chunk = JSON.parse(data) } catch { continue }
|
|
92
|
+
const ev = parser.handle(null, chunk)
|
|
93
|
+
if (ev.error) inband = ev.error
|
|
94
|
+
if (typeof ev.reasoning === 'string') reasoning += ev.reasoning
|
|
95
|
+
if (typeof ev.text === 'string') text += ev.text
|
|
96
|
+
}
|
|
97
|
+
const usage = parser.usage()
|
|
98
|
+
return {
|
|
99
|
+
arm: label,
|
|
100
|
+
level: level ?? null,
|
|
101
|
+
http: res.status,
|
|
102
|
+
reasoningChars: reasoning.length,
|
|
103
|
+
textChars: text.length,
|
|
104
|
+
reasoningHead: reasoning.slice(0, 40),
|
|
105
|
+
providerModel: parser.providerModel(),
|
|
106
|
+
finish: parser.finish(),
|
|
107
|
+
usage,
|
|
108
|
+
inband,
|
|
109
|
+
bytes: raw.length,
|
|
110
|
+
rawHead: raw.slice(0, 700),
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(`model=${MODEL} levels=${JSON.stringify(LEVELS)} repeat=${REPEAT}`)
|
|
115
|
+
const arms = []
|
|
116
|
+
for (let i = 0; i < REPEAT; i++) {
|
|
117
|
+
arms.push(await arm(`baseline#${i + 1}`, undefined))
|
|
118
|
+
for (const level of LEVELS) arms.push(await arm(`${level}#${i + 1}`, level))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log('\narm http reasoning text providerModel finish inband')
|
|
122
|
+
for (const a of arms) {
|
|
123
|
+
console.log(`${a.arm.padEnd(18)} ${String(a.http).padEnd(5)} ${String(a.reasoningChars).padStart(9)} ${String(a.textChars).padStart(5)} ${String(a.providerModel ?? '-').padEnd(20)} ${String(a.finish ?? '-').padEnd(7)} ${a.inband ? JSON.stringify(a.inband).slice(0, 60) : ''}`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const mean = (xs) => (xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0)
|
|
127
|
+
const baseVals = arms.filter((a) => a.level === null).map((a) => a.reasoningChars)
|
|
128
|
+
const baseMean = mean(baseVals)
|
|
129
|
+
console.log(`\nbaseline reasoning 均值 ${Math.round(baseMean)}(n=${baseVals.length})`)
|
|
130
|
+
const verdicts = {}
|
|
131
|
+
for (const level of LEVELS) {
|
|
132
|
+
const vals = arms.filter((a) => a.level === level).map((a) => a.reasoningChars)
|
|
133
|
+
const m = mean(vals)
|
|
134
|
+
const allAbove = vals.every((v) => v > baseMean)
|
|
135
|
+
const allBelow = vals.every((v) => v < baseMean)
|
|
136
|
+
const verdict = allAbove || allBelow ? 'DIRECTION-CONSISTENT' : 'INCONCLUSIVE'
|
|
137
|
+
verdicts[level] = { vals, mean: Math.round(m), verdict }
|
|
138
|
+
console.log(` ${level.padEnd(8)} 均值 ${String(Math.round(m)).padStart(5)} 各次 ${JSON.stringify(vals)} → ${verdict}`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
mkdirSync('docs/probes', { recursive: true })
|
|
142
|
+
const out = `docs/probes/trae-efforts-${Date.now()}.json`
|
|
143
|
+
writeFileSync(out, JSON.stringify({
|
|
144
|
+
at: new Date().toISOString(),
|
|
145
|
+
model: MODEL, levels: LEVELS, repeat: REPEAT, prompt: PROMPT,
|
|
146
|
+
baselineMean: Math.round(baseMean), verdicts, arms,
|
|
147
|
+
}, null, 2))
|
|
148
|
+
console.log('\n证据 →', out)
|
|
149
|
+
console.log('判读纪律:DIRECTION-CONSISTENT 才说明该档位**可能**改变上游思考量;INCONCLUSIVE 不下结论(自适应非单调 + 小样本)。')
|