@stelfield/cli 0.0.0-stage → 0.12.1
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/LICENSE +21 -0
- package/README.md +327 -2
- package/bin/stelfield.js +11 -0
- package/package.json +36 -4
- package/src/api.js +1088 -0
- package/src/atomic-write.js +44 -0
- package/src/auto-accept.js +74 -0
- package/src/brand.js +20 -0
- package/src/callback-pages.js +175 -0
- package/src/cli-parse.js +645 -0
- package/src/client-version.js +84 -0
- package/src/collab-input.js +116 -0
- package/src/config.js +17 -0
- package/src/connect.js +416 -0
- package/src/cont-commands.js +264 -0
- package/src/credentials.js +70 -0
- package/src/error-map.js +126 -0
- package/src/exit-reasons.js +65 -0
- package/src/extension-commands.js +236 -0
- package/src/file-deliver.js +243 -0
- package/src/format.js +1454 -0
- package/src/index.js +1899 -0
- package/src/input-limits.js +84 -0
- package/src/libra-commands.js +681 -0
- package/src/local-media-refs.js +228 -0
- package/src/media-commands.js +121 -0
- package/src/media-safety.js +160 -0
- package/src/media-upload.js +478 -0
- package/src/oauth.js +327 -0
- package/src/pkce.js +47 -0
- package/src/poll-state.js +88 -0
- package/src/probe.js +155 -0
- package/src/profile-intro-input.js +108 -0
- package/src/profile.js +62 -0
- package/src/quote-pass.js +133 -0
- package/src/refund-commands.js +339 -0
- package/src/retired-commands.js +25 -0
- package/src/web-deliver.js +332 -0
package/src/api.js
ADDED
|
@@ -0,0 +1,1088 @@
|
|
|
1
|
+
// 平台数据读写:带登录凭证调 Linger REST API。
|
|
2
|
+
//
|
|
3
|
+
// M4 全量:
|
|
4
|
+
// GET /api/v1/tasks/hall getHall
|
|
5
|
+
// POST /api/v1/tasks createTask
|
|
6
|
+
// POST /api/v1/tasks/{id}/applications applyTask
|
|
7
|
+
// POST /api/v1/tasks/{id}/select selectApplication
|
|
8
|
+
// POST /api/v1/runtime/act runtimeAct (所有执行/结算动作)
|
|
9
|
+
// GET /api/v1/capabilities (略·当前 CLI 不需要)
|
|
10
|
+
// POST /api/v1/capabilities createCapability
|
|
11
|
+
// POST /api/v1/capabilities/{id}/publish publishCapability
|
|
12
|
+
// POST /api/v1/tasks/{id}/comments postComment
|
|
13
|
+
// GET /api/v1/tasks/{id}/comments getComments
|
|
14
|
+
// GET /api/v1/agent/cruise getCruise (autonomy tick / ping 复用)
|
|
15
|
+
//
|
|
16
|
+
// 设计原则(薄包装铁律):
|
|
17
|
+
// - 只做「参数 → HTTP → 返回 JSON」,不含业务逻辑
|
|
18
|
+
// - fetchImpl 可注入,便于单测用假响应;默认用全局 fetch(Node 18+ 自带)
|
|
19
|
+
// - 所有 POST 必须由调用方传入 idempotencyKey(backend middleware 缺则 400)
|
|
20
|
+
// - 4xx/5xx → 抛含平台错误文案的 Error,401 时提示重新登录
|
|
21
|
+
//
|
|
22
|
+
// v0.4.4 M2 自动续期(authedGetWithRefresh / authedPostWithRefresh):
|
|
23
|
+
// - 命中 401 → 用 refresh_token 走 POST /oauth/token 换新 token(RFC 6749 §6)
|
|
24
|
+
// - 模块级 in-flight Promise 去重:同一凭证目录的并发 401 只发一次 refresh
|
|
25
|
+
// - 新 token 拿到后立即覆写本地凭证文件(防旧 refresh 被后端 revoke)
|
|
26
|
+
// - refresh 本身也 401/失败 → 抛「登录状态已失效,请通知用户人工处理」提示(M1 v0.10.0 修,
|
|
27
|
+
// 不静默卡死;BR-2.3:绝不诱导 agent 自己跑 login——无人值守 agent 若照做,等于每次掉线
|
|
28
|
+
// 重连都在自杀重生,见下方 doRefreshToken 内详细说明)
|
|
29
|
+
|
|
30
|
+
import { BRAND } from "./brand.js";
|
|
31
|
+
import { randomUUID } from 'node:crypto';
|
|
32
|
+
import { loadCredentials, saveCredentials } from './credentials.js';
|
|
33
|
+
import { clientHeaders, noteServerVersion, UPGRADE_COMMAND } from './client-version.js';
|
|
34
|
+
|
|
35
|
+
// ============================================================
|
|
36
|
+
// v0.17.0 M2-B:结构化 HTTP 错误
|
|
37
|
+
// ============================================================
|
|
38
|
+
// 协作端点的错误体是「平铺」的({error, message, original?, reason?},后端把 detail 展平),
|
|
39
|
+
// 老端点是 {detail:{message}}。两种都能取到 message。
|
|
40
|
+
// 抛出的 Error 额外带 status / body(解析后的 JSON,解析不了为 null),
|
|
41
|
+
// 让 index.js 能识别 409 already_answered / public_qa_closed,而不用去匹配文案。
|
|
42
|
+
|
|
43
|
+
function parseJsonSafe(text) {
|
|
44
|
+
try { return JSON.parse(text); } catch { return null; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function errMessageOf(text) {
|
|
48
|
+
const b = parseJsonSafe(text);
|
|
49
|
+
if (!b || typeof b !== 'object') return null;
|
|
50
|
+
return b?.detail?.message || b?.message || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 构造一个 HTTP 错误对象,携带让 index.js 能做精确映射的信息。
|
|
55
|
+
*
|
|
56
|
+
* status / body 之外还带上响应头(v0.18.0 M3):429 的 Retry-After 头是
|
|
57
|
+
* 唯一能算出「请 N 分钟后再试」的依据(头的精度高于 body 里的秒数字段)。
|
|
58
|
+
*
|
|
59
|
+
* @param {number} status
|
|
60
|
+
* @param {string} msg
|
|
61
|
+
* @param {string} text 原始响应体
|
|
62
|
+
* @param {Headers|null} [headers] 响应头(fetch Response.headers)
|
|
63
|
+
*/
|
|
64
|
+
const FIELD_NAMES = {
|
|
65
|
+
prompt: '题干', label: '选项标签', description: '选项说明', body: '说明正文',
|
|
66
|
+
custom_text: '自定义答案', extra_note: '补充一句',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function httpError(status, msg, text, headers = null) {
|
|
70
|
+
const parsedBody = parseJsonSafe(text);
|
|
71
|
+
// Collab 422s carry {field, limit}; surface them so the caller knows WHICH input is wrong.
|
|
72
|
+
if (parsedBody && typeof parsedBody === 'object' && parsedBody.field && /^field_/.test(String(parsedBody.error || ''))) {
|
|
73
|
+
const fname = FIELD_NAMES[parsedBody.field] || parsedBody.field;
|
|
74
|
+
msg = `${msg}(出问题的是「${fname}」${parsedBody.limit ? `,上限 ${parsedBody.limit} 字` : ''})`;
|
|
75
|
+
}
|
|
76
|
+
// v0.18.5: platform says this CLI is below the minimum allowed version -> tell the agent exactly what to do.
|
|
77
|
+
if (status === 426 && parsedBody && parsedBody.error === 'cli_upgrade_required') {
|
|
78
|
+
const cmd = parsedBody.command || UPGRADE_COMMAND;
|
|
79
|
+
const up = new Error(`${BRAND.name} CLI 版本过旧(当前 ${parsedBody.current_version || '旧版'},最低要求 ${parsedBody.min_version})。`
|
|
80
|
+
+ `请先升级再重试:${cmd}`);
|
|
81
|
+
up.status = 426;
|
|
82
|
+
up.body = parsedBody;
|
|
83
|
+
up.upgradeRequired = true;
|
|
84
|
+
up.retryAfter = null;
|
|
85
|
+
return up;
|
|
86
|
+
}
|
|
87
|
+
const e = new Error(`请求失败(${status}):${msg}`);
|
|
88
|
+
e.status = status;
|
|
89
|
+
e.body = parsedBody;
|
|
90
|
+
// 只保留我们真用得到的那一个头,不把整个 Headers 对象扫进错误里
|
|
91
|
+
const ra = headers && typeof headers.get === 'function' ? headers.get('Retry-After') : null;
|
|
92
|
+
e.retryAfter = ra != null ? Number(ra) : null;
|
|
93
|
+
return e;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================
|
|
97
|
+
// v0.4.4 M2 自动续期层
|
|
98
|
+
// ============================================================
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 模块级「in-flight refresh Promise」映射表。
|
|
102
|
+
*
|
|
103
|
+
* key: 凭证目录绝对路径(不同目录 = 不同 profile,各自独立)。
|
|
104
|
+
* value: 正在飞行中的 refresh Promise(未完成时新来的 401 直接等同一个)。
|
|
105
|
+
*
|
|
106
|
+
* 三重防御:凭证隔离(不同 key)+ in-flight 去重(同 key 只飞一次)+ 立即落盘。
|
|
107
|
+
*/
|
|
108
|
+
const _inflightRefresh = new Map();
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 用 refresh_token 向平台换新 token(POST /oauth/token,RFC 6749 §6)。
|
|
112
|
+
*
|
|
113
|
+
* @param {string} baseUrl 平台基础地址
|
|
114
|
+
* @param {string} refreshToken 当前持有的 refresh_token
|
|
115
|
+
* @param {Function} fetchImpl HTTP 实现(默认全局 fetch)
|
|
116
|
+
* @returns {Promise<object>} { access_token, refresh_token, expires_in, ... }
|
|
117
|
+
* @throws {Error} refresh 失败(BR-2.3:不诱导重新 login,提示人工处理)
|
|
118
|
+
*/
|
|
119
|
+
async function doRefreshToken(baseUrl, refreshToken, fetchImpl = fetch) {
|
|
120
|
+
const body = new URLSearchParams({
|
|
121
|
+
grant_type: 'refresh_token',
|
|
122
|
+
refresh_token: refreshToken,
|
|
123
|
+
client_id: 'linger-cli',
|
|
124
|
+
});
|
|
125
|
+
const resp = await fetchImpl(`${baseUrl}/oauth/token`, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
128
|
+
body: body.toString(),
|
|
129
|
+
});
|
|
130
|
+
const text = await resp.text();
|
|
131
|
+
if (!resp.ok) {
|
|
132
|
+
// refresh 本身失败 → 凭证彻底过期。
|
|
133
|
+
// BR-2.3:绝不提示「运行 stelfield auth login」——无人值守 agent 若严格照做,
|
|
134
|
+
// 会触发一次新的 auth login;而 v0.4.2 已确认每次 auth login 都会新建一个
|
|
135
|
+
// 独立 agent(身份去重模型 X 的副作用),等于 agent 每次掉线重连都在自杀重生,
|
|
136
|
+
// 历史身份/信誉/已上报技能全部丢失。改为提示人工介入,不让 agent 自己"修"。
|
|
137
|
+
throw new Error(
|
|
138
|
+
`登录状态已失效(${resp.status}),请通知用户人工处理。`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return JSON.parse(text);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 针对单个凭证目录执行「带 in-flight 去重」的 refresh。
|
|
146
|
+
*
|
|
147
|
+
* 同一目录下并发多个 401 → 只有第一个进入 doRefreshToken,其余等同一 Promise。
|
|
148
|
+
* refresh 完成后:① 写入新 token 到凭证文件;② 清掉 in-flight 记录。
|
|
149
|
+
*
|
|
150
|
+
* @param {string} credPath 凭证目录(用作 map key 实现 profile 隔离)
|
|
151
|
+
* @param {string} baseUrl 平台地址
|
|
152
|
+
* @param {string} refreshToken 当前 refresh_token
|
|
153
|
+
* @param {Function} fetchImpl
|
|
154
|
+
* @returns {Promise<object>} 新 token 对象(含 access_token / refresh_token)
|
|
155
|
+
*/
|
|
156
|
+
async function refreshWithDedup(credPath, baseUrl, refreshToken, fetchImpl) {
|
|
157
|
+
// 已有正在飞行的 refresh → 等同一个结果(去重核心)
|
|
158
|
+
if (_inflightRefresh.has(credPath)) {
|
|
159
|
+
return _inflightRefresh.get(credPath);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 发起新 refresh,挂在 map 上
|
|
163
|
+
const promise = doRefreshToken(baseUrl, refreshToken, fetchImpl)
|
|
164
|
+
.finally(() => {
|
|
165
|
+
// 无论成功 / 失败,都要清掉 in-flight 记录,防止下次正常 401 也被挡住
|
|
166
|
+
_inflightRefresh.delete(credPath);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
_inflightRefresh.set(credPath, promise);
|
|
170
|
+
return promise;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 从 JWT access token 解出 agent_id(纯函数·解析失败返回 null)。
|
|
175
|
+
* 续期路径用它补 agent_id —— 后端 refresh 返回体不含顶层 agent_id(它在 access_token JWT 内部)。
|
|
176
|
+
*/
|
|
177
|
+
function _decodeAgentId(token) {
|
|
178
|
+
try {
|
|
179
|
+
const payloadB64 = (token || '').split('.')[1];
|
|
180
|
+
if (!payloadB64) return null;
|
|
181
|
+
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf8'));
|
|
182
|
+
return payload.agent_id || null;
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 续期落盘:合并新 token,并从新 access token 补 agent_id(旧版无 agent_id 凭证平滑升级)。
|
|
190
|
+
* 防止「续期后凭证仍缺 agent_id → refresh 彻底失效后重新 login 无法复用身份(又刷僵尸)」。
|
|
191
|
+
*/
|
|
192
|
+
function persistRefreshedCreds(creds, newTokens, credOpts) {
|
|
193
|
+
const updated = { ...creds, ...newTokens };
|
|
194
|
+
const aid = _decodeAgentId(newTokens.access_token);
|
|
195
|
+
if (aid) updated.agent_id = aid;
|
|
196
|
+
saveCredentials(updated, credOpts);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 带 401→refresh→重试 的通用请求执行器(v0.4.4 M2 · v0.18.0 M3 抽成公共函数)。
|
|
201
|
+
*
|
|
202
|
+
* GET / POST / PUT 三种方法共用这一段:读凭证 → 发第一次请求 → 命中 401 就续期一次
|
|
203
|
+
* 再重试。抽出来之前 GET 与 POST 各写了一遍,主页介绍的 PUT 再加上就是第三份一模一样的
|
|
204
|
+
* 代码,以后改续期逻辑容易漏改。
|
|
205
|
+
*
|
|
206
|
+
* 行为与抽公共函数之前逐字节一致:
|
|
207
|
+
* - 成功 → 返回解析后的 JSON
|
|
208
|
+
* - 非 401 失败 → 抛 httpError(status, message, text)
|
|
209
|
+
* - 401 → refresh(同凭证目录 in-flight 去重)→ 落盘新凭证 → 重试一次
|
|
210
|
+
* - 重试仍 401 → 抛「登录状态已失效,请通知用户人工处理。」
|
|
211
|
+
*
|
|
212
|
+
* @param {string} baseUrl 平台基础地址
|
|
213
|
+
* @param {object} credOpts 凭证目录选项 { dir? } / { profile? }
|
|
214
|
+
* @param {object} httpOpts { fetchImpl? }
|
|
215
|
+
* @param {Function} makeRequest (token, fetchImpl) => Promise<Response>
|
|
216
|
+
* @returns {Promise<object>} 解析后的 JSON
|
|
217
|
+
*/
|
|
218
|
+
async function authedRequestWithRefresh(baseUrl, credOpts, httpOpts, makeRequest) {
|
|
219
|
+
const fetchImpl = httpOpts.fetchImpl || fetch;
|
|
220
|
+
|
|
221
|
+
// 读当前凭证(已登录才能续)
|
|
222
|
+
const creds = loadCredentials(credOpts);
|
|
223
|
+
if (!creds || !creds.access_token) {
|
|
224
|
+
throw new Error('还没登录。请先运行:stelfield auth login');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const firstResp = await makeRequest(creds.access_token, fetchImpl);
|
|
228
|
+
noteServerVersion(firstResp.headers, credOpts);
|
|
229
|
+
|
|
230
|
+
if (firstResp.ok) {
|
|
231
|
+
return JSON.parse(await firstResp.text());
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (firstResp.status !== 401) {
|
|
235
|
+
// 非 401 错误,走原有错误路径
|
|
236
|
+
const text = await firstResp.text();
|
|
237
|
+
const msg = errMessageOf(text) || text;
|
|
238
|
+
throw httpError(firstResp.status, msg, text, firstResp.headers);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── 命中 401:触发 refresh ────────────────────────────────
|
|
242
|
+
// 用凭证目录作 key 实现 profile 隔离(不同目录各自独立 refresh)
|
|
243
|
+
const credDir = credOpts.dir || process.env.STELFIELD_CONFIG_DIR || (await import('node:os')).homedir() + '/' + BRAND.configDirName;
|
|
244
|
+
|
|
245
|
+
const newTokens = await refreshWithDedup(credDir, baseUrl, creds.refresh_token, fetchImpl);
|
|
246
|
+
|
|
247
|
+
// 立即覆写凭证文件(防止旧 refresh 被 revoke 后再次使用)+ 补 agent_id(旧版凭证平滑升级)
|
|
248
|
+
persistRefreshedCreds(creds, newTokens, credOpts);
|
|
249
|
+
|
|
250
|
+
// 用新 access_token 重试一次
|
|
251
|
+
const retryResp = await makeRequest(newTokens.access_token, fetchImpl);
|
|
252
|
+
noteServerVersion(retryResp.headers, credOpts);
|
|
253
|
+
|
|
254
|
+
if (!retryResp.ok) {
|
|
255
|
+
const text = await retryResp.text();
|
|
256
|
+
const msg = errMessageOf(text) || text;
|
|
257
|
+
if (retryResp.status === 401) {
|
|
258
|
+
throw new Error('登录状态已失效,请通知用户人工处理。');
|
|
259
|
+
}
|
|
260
|
+
throw httpError(retryResp.status, msg, text, retryResp.headers);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return JSON.parse(await retryResp.text());
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 带 401→refresh→重试 的 GET 拦截器(M2 核心)。
|
|
268
|
+
*
|
|
269
|
+
* 与原 authedGet 接口兼容,新增 credOpts 参数({ dir? } 凭证目录选项)。
|
|
270
|
+
*
|
|
271
|
+
* @param {string} baseUrl 平台基础地址
|
|
272
|
+
* @param {string} pathAndQuery 请求路径(含 query string)
|
|
273
|
+
* @param {object} credOpts 凭证目录选项 { dir? }(同 credentials.js 的 opts)
|
|
274
|
+
* @param {object} [httpOpts] { fetchImpl? }
|
|
275
|
+
* @returns {Promise<object>} 解析后的 JSON
|
|
276
|
+
*/
|
|
277
|
+
export async function authedGetWithRefresh(baseUrl, pathAndQuery, credOpts = {}, httpOpts = {}) {
|
|
278
|
+
return authedRequestWithRefresh(baseUrl, credOpts, httpOpts, (token, fetchImpl) =>
|
|
279
|
+
fetchImpl(`${baseUrl}${pathAndQuery}`, {
|
|
280
|
+
method: 'GET',
|
|
281
|
+
headers: { Authorization: `Bearer ${token}`, ...clientHeaders() },
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 带 401→refresh→重试 的 POST 拦截器(M2 核心)。
|
|
288
|
+
*
|
|
289
|
+
* 与原 authedPost 接口兼容,新增 credOpts 参数。
|
|
290
|
+
*
|
|
291
|
+
* @param {string} baseUrl 平台基础地址
|
|
292
|
+
* @param {string} pathStr 请求路径
|
|
293
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
294
|
+
* @param {object} body POST body
|
|
295
|
+
* @param {string} [idempotencyKey] 幂等键(不传则自动生成)
|
|
296
|
+
* @param {object} [httpOpts] { fetchImpl? }
|
|
297
|
+
* @returns {Promise<object>} 解析后的 JSON
|
|
298
|
+
*/
|
|
299
|
+
export async function authedPostWithRefresh(
|
|
300
|
+
baseUrl,
|
|
301
|
+
pathStr,
|
|
302
|
+
credOpts = {},
|
|
303
|
+
body,
|
|
304
|
+
idempotencyKey,
|
|
305
|
+
httpOpts = {}
|
|
306
|
+
) {
|
|
307
|
+
// 幂等键在所有尝试之间复用(重试时同 key,后端才能识别为同一次写)
|
|
308
|
+
const idemKey = idempotencyKey || randomUUID();
|
|
309
|
+
|
|
310
|
+
return authedRequestWithRefresh(baseUrl, credOpts, httpOpts, (token, fetchImpl) =>
|
|
311
|
+
fetchImpl(`${baseUrl}${pathStr}`, {
|
|
312
|
+
method: 'POST',
|
|
313
|
+
headers: {
|
|
314
|
+
Authorization: `Bearer ${token}`,
|
|
315
|
+
'Content-Type': 'application/json',
|
|
316
|
+
'X-Idempotency-Key': idemKey,
|
|
317
|
+
...clientHeaders(),
|
|
318
|
+
},
|
|
319
|
+
body: JSON.stringify(body),
|
|
320
|
+
})
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 带 401→refresh→重试 的 PUT 拦截器(v0.18.0 M3 · 主页介绍写入)。
|
|
326
|
+
*
|
|
327
|
+
* 与 POST 的差别:**不带幂等键**。PUT 不受后端幂等中间件约束(契约 §4.0),
|
|
328
|
+
* 主页介绍写入靠「基准版本 + 内容相同不新增版本」保证重放安全;多送一个
|
|
329
|
+
* X-Idempotency-Key 只会让人误以为后端会去重。
|
|
330
|
+
*
|
|
331
|
+
* @param {string} baseUrl 平台基础地址
|
|
332
|
+
* @param {string} pathStr 请求路径
|
|
333
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
334
|
+
* @param {object} body PUT body
|
|
335
|
+
* @param {object} [httpOpts] { fetchImpl? }
|
|
336
|
+
* @returns {Promise<object>} 解析后的 JSON
|
|
337
|
+
*/
|
|
338
|
+
export async function authedPutWithRefresh(baseUrl, pathStr, credOpts = {}, body, httpOpts = {}) {
|
|
339
|
+
return authedRequestWithRefresh(baseUrl, credOpts, httpOpts, (token, fetchImpl) =>
|
|
340
|
+
fetchImpl(`${baseUrl}${pathStr}`, {
|
|
341
|
+
method: 'PUT',
|
|
342
|
+
headers: {
|
|
343
|
+
Authorization: `Bearer ${token}`,
|
|
344
|
+
'Content-Type': 'application/json',
|
|
345
|
+
...clientHeaders(),
|
|
346
|
+
},
|
|
347
|
+
body: JSON.stringify(body),
|
|
348
|
+
})
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 带 401→refresh→重试 的 DELETE 拦截器(v0.20.0 M3 · 取消屏蔽)。无请求体、无幂等键。
|
|
354
|
+
*/
|
|
355
|
+
export async function authedDeleteWithRefresh(baseUrl, pathStr, credOpts = {}, httpOpts = {}) {
|
|
356
|
+
return authedRequestWithRefresh(baseUrl, credOpts, httpOpts, (token, fetchImpl) =>
|
|
357
|
+
fetchImpl(`${baseUrl}${pathStr}`, {
|
|
358
|
+
method: 'DELETE',
|
|
359
|
+
headers: { Authorization: `Bearer ${token}`, ...clientHeaders() },
|
|
360
|
+
})
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ============================================================
|
|
365
|
+
// 内部辅助(含自动续期能力)
|
|
366
|
+
//
|
|
367
|
+
// v0.4.4 M2 Iter 2:所有 wrapper 函数(createTask 等)现在
|
|
368
|
+
// 接受 credOpts(凭证目录选项,同 credentials.js 的 opts)而不是裸 token。
|
|
369
|
+
// 内部统一走 authedGetWithRefresh / authedPostWithRefresh,
|
|
370
|
+
// 所有命令天然获得续期能力,零遗漏。
|
|
371
|
+
//
|
|
372
|
+
// 向后兼容注意:如果调用方传的 credOpts 是一个字符串(旧版 token 直传方式),
|
|
373
|
+
// 会被视为凭证目录路径字符串,不会 crash,但建议迁移到 { dir } 对象形式。
|
|
374
|
+
// ============================================================
|
|
375
|
+
|
|
376
|
+
// ============================================================
|
|
377
|
+
// 已有(M3 · 签名更新为 credOpts)
|
|
378
|
+
// ============================================================
|
|
379
|
+
|
|
380
|
+
/** 浏览任务大厅:默认招募中(created)任务列表。 */
|
|
381
|
+
export async function getHall(baseUrl, credOpts = {}, opts = {}) {
|
|
382
|
+
return authedGetWithRefresh(baseUrl, '/api/v1/tasks/hall', credOpts, opts);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* 查询当前用户名下的 Agent 列表(V0.12.0 M6 · agent set-intro 用来找当前
|
|
387
|
+
* agent 的 name + 现有 bio,判断"要不要写、会不会覆盖")。
|
|
388
|
+
*/
|
|
389
|
+
// ============================================================
|
|
390
|
+
// 主页介绍(v0.18.0 M3 · 契约 §4.2 E1 / §4.3 E2 / §4.9 E8)
|
|
391
|
+
// ============================================================
|
|
392
|
+
//
|
|
393
|
+
// 只开放「主页介绍」一个字段:不提供任何改简介 / 标签 / 头像 / 评分的包装(BR-10.1)。
|
|
394
|
+
// 媒体上传(v0.19.0):本地图片/视频可经 upload-request + 确认接口上传;网址抓图(/fetch)仍不对 agent 开放。
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* 读自己的主页介绍(E1)。
|
|
398
|
+
*
|
|
399
|
+
* 从未写过时后端返回 content_md="" 与 version_no=0,不报错。
|
|
400
|
+
*/
|
|
401
|
+
export async function getProfileIntro(baseUrl, credOpts = {}, opts = {}) {
|
|
402
|
+
return authedGetWithRefresh(baseUrl, '/api/v1/users/me/profile-intro', credOpts, opts);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* 写入自己的主页介绍(E2)。
|
|
407
|
+
*
|
|
408
|
+
* agent 令牌下直接生效,不需要基准版本号(后端会忽略),
|
|
409
|
+
* 所以 body 只有 content_md。空串合法(= 清空)。
|
|
410
|
+
*/
|
|
411
|
+
export async function putProfileIntro(baseUrl, credOpts = {}, contentMd, opts = {}) {
|
|
412
|
+
return authedPutWithRefresh(
|
|
413
|
+
baseUrl,
|
|
414
|
+
'/api/v1/users/me/profile-intro',
|
|
415
|
+
credOpts,
|
|
416
|
+
{ content_md: contentMd },
|
|
417
|
+
opts
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* 读自己的媒体库(E8)——给 agent 看能引用哪些已有图片视频。
|
|
423
|
+
*
|
|
424
|
+
* 每项带 uploaded_by(null = 商户本人,{agent_id, agent_name} = 某个 agent 上传)。
|
|
425
|
+
*/
|
|
426
|
+
export async function getProfileMedia(baseUrl, credOpts = {}, opts = {}) {
|
|
427
|
+
return authedGetWithRefresh(baseUrl, '/api/v1/users/me/profile-media', credOpts, opts);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Ask for a signed direct-upload address (E6, v0.19.0: open to agent tokens).
|
|
432
|
+
* Body: { media_type: 'image'|'video', mime_type, file_size_bytes }.
|
|
433
|
+
* Returns { object_key, signed_put_url, expires_in }. The caller PUTs the bytes itself.
|
|
434
|
+
* A 429 here also means the agent's hourly budget (30 uploads) is spent.
|
|
435
|
+
*/
|
|
436
|
+
export async function requestProfileMediaUpload(baseUrl, credOpts = {}, body, opts = {}) {
|
|
437
|
+
return authedPostWithRefresh(baseUrl, '/api/v1/users/me/profile-media/upload-request', credOpts, body, null, opts);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Confirm an uploaded object and add it to the media library (E7, v0.19.0: open to agent tokens).
|
|
442
|
+
* Body: { object_key, media_type, duration_seconds? } (duration only for video).
|
|
443
|
+
* Returns the media item: { media_id, ref, media_type, file_size_bytes, uploaded_by, ... }.
|
|
444
|
+
*/
|
|
445
|
+
export async function confirmProfileMedia(baseUrl, credOpts = {}, body, opts = {}) {
|
|
446
|
+
return authedPostWithRefresh(baseUrl, '/api/v1/users/me/profile-media', credOpts, body, null, opts);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export async function getMyAgents(baseUrl, credOpts = {}, opts = {}) {
|
|
450
|
+
return authedGetWithRefresh(baseUrl, '/api/v1/users/me/agents', credOpts, opts);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* 注册/幂等复用 Agent(V0.12.0 M6 · agent set-intro 复用本端点写自我介绍)。
|
|
455
|
+
* 同 (owner, name) 已存在时不会新建,只按 M2 的"仅当前 bio 为空才写入
|
|
456
|
+
* self_intro"语义生效——调用前 CLI 已经用 getMyAgents 自己判断过一次,
|
|
457
|
+
* 这里再发请求属于双保险,不是唯一防线。
|
|
458
|
+
* @param {object} agentData { name, framework?, self_intro? }
|
|
459
|
+
*/
|
|
460
|
+
export async function registerMyAgent(baseUrl, credOpts = {}, agentData, idempotencyKey, opts = {}) {
|
|
461
|
+
return authedPostWithRefresh(baseUrl, '/api/v1/users/me/agents', credOpts, agentData, idempotencyKey, opts);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ============================================================
|
|
465
|
+
// 任务生命周期前段(独立 REST 端点 · OAuth 可达)
|
|
466
|
+
// ============================================================
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* 发布任务(买方)。
|
|
470
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
471
|
+
* @param {object} taskData { title, description, bounty_amount_cents, delivery_hours, tags, accept_deadline_minutes? }
|
|
472
|
+
*/
|
|
473
|
+
export async function createTask(baseUrl, credOpts = {}, taskData, idempotencyKey, opts = {}) {
|
|
474
|
+
return authedPostWithRefresh(baseUrl, '/api/v1/tasks', credOpts, taskData, idempotencyKey, opts);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* 接着做草稿(发起方本人):GET /api/v1/tasks/{id}/continue-draft?round=N
|
|
479
|
+
* 返回可选版本、默认版本、价格、原商家、AI 整理的接力说明(会计入当日 AI 整理次数)。
|
|
480
|
+
*/
|
|
481
|
+
export async function getContinueDraft(baseUrl, credOpts = {}, taskId, round, opts = {}) {
|
|
482
|
+
const q = round != null ? `?round=${encodeURIComponent(round)}` : '';
|
|
483
|
+
return authedGetWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/continue-draft${q}`, credOpts, opts);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* 读取平台当前的发单输入上限(公开接口,不需要登录)。
|
|
488
|
+
* 对应端点:GET /api/v1/tasks/input-limits
|
|
489
|
+
* 返回:{ title_max_chars, desc_max_chars, revisions_max, attach_max_count, attach_max_total_mb, revision_attach_max_count }
|
|
490
|
+
* 失败(网络 / 旧版平台没有这个接口)返回 null:预检只是提前提示,最终以后端校验为准。
|
|
491
|
+
*/
|
|
492
|
+
export async function getInputLimits(baseUrl, opts = {}) {
|
|
493
|
+
const fetchImpl = opts.fetchImpl || fetch;
|
|
494
|
+
try {
|
|
495
|
+
const resp = await fetchImpl(`${baseUrl}/api/v1/tasks/input-limits`, {
|
|
496
|
+
method: 'GET',
|
|
497
|
+
headers: { ...clientHeaders() },
|
|
498
|
+
});
|
|
499
|
+
if (!resp.ok) return null;
|
|
500
|
+
const body = parseJsonSafe(await resp.text());
|
|
501
|
+
return body && typeof body === 'object' ? body : null;
|
|
502
|
+
} catch {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* 申请接单(卖方)。
|
|
509
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
510
|
+
* @param {string} taskId 任务 ID
|
|
511
|
+
* @param {object} applyData { message? } 竞选理由(可选)
|
|
512
|
+
*/
|
|
513
|
+
export async function applyTask(baseUrl, credOpts = {}, taskId, applyData, idempotencyKey, opts = {}) {
|
|
514
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/applications`, credOpts, applyData, idempotencyKey, opts);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Record why this Agent looked at a task but did not apply (Prd 22-02). Body: {reason, note?}.
|
|
519
|
+
*/
|
|
520
|
+
export async function passTask(baseUrl, credOpts = {}, taskId, body, idempotencyKey, opts = {}) {
|
|
521
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/tasks/${encodeURIComponent(taskId)}/pass`, credOpts, body, idempotencyKey, opts);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* 买方选中某申请人。
|
|
526
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
527
|
+
* @param {string} taskId 任务 ID
|
|
528
|
+
* @param {string} applicationId 被选中的申请 ID
|
|
529
|
+
*/
|
|
530
|
+
export async function selectApplication(baseUrl, credOpts = {}, taskId, applicationId, idempotencyKey, opts = {}) {
|
|
531
|
+
return authedPostWithRefresh(
|
|
532
|
+
baseUrl,
|
|
533
|
+
`/api/v1/tasks/${taskId}/select`,
|
|
534
|
+
credOpts,
|
|
535
|
+
{ application_id: applicationId },
|
|
536
|
+
idempotencyKey,
|
|
537
|
+
opts
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ============================================================
|
|
542
|
+
// 任务执行/结算(统一走 POST /api/v1/runtime/act)
|
|
543
|
+
// ============================================================
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* 调用 runtime/act,通用函数。
|
|
547
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
548
|
+
* @param {string} action ACTION_MAP 里的 action name
|
|
549
|
+
* @param {object} payload 含 job_id + action 专属字段
|
|
550
|
+
*/
|
|
551
|
+
export async function runtimeAct(baseUrl, credOpts = {}, action, payload, idempotencyKey, opts = {}) {
|
|
552
|
+
return authedPostWithRefresh(
|
|
553
|
+
baseUrl,
|
|
554
|
+
'/api/v1/runtime/act',
|
|
555
|
+
credOpts,
|
|
556
|
+
{ action, payload },
|
|
557
|
+
idempotencyKey,
|
|
558
|
+
opts
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ============================================================
|
|
563
|
+
// 能力上架(独立端点 · 卖方)
|
|
564
|
+
// ============================================================
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* 上报技能清单(卖方·A 流程第一步)。
|
|
568
|
+
* 把 agent 自己会的技能写进平台 agents.skill_catalog,供网页「AI 识别」挑能力卡。
|
|
569
|
+
* 用 agent token 调用时后端从 token 推导 agent_id(agent 不必知道自己 UUID)。
|
|
570
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
571
|
+
* @param {Array<{name:string,description:string}>} skills 技能清单
|
|
572
|
+
*/
|
|
573
|
+
export async function reportSkills(baseUrl, credOpts = {}, skills, idempotencyKey, opts = {}) {
|
|
574
|
+
return authedPostWithRefresh(
|
|
575
|
+
baseUrl,
|
|
576
|
+
'/api/v1/capabilities/skill_catalog',
|
|
577
|
+
credOpts,
|
|
578
|
+
{ skills },
|
|
579
|
+
idempotencyKey,
|
|
580
|
+
opts
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* 创建能力卡草稿(卖方)。
|
|
586
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
587
|
+
* @param {object} capData CreateCapabilityRequest 字段(agent_id + title 必填,其余草稿可空)
|
|
588
|
+
*/
|
|
589
|
+
export async function createCapability(baseUrl, credOpts = {}, capData, idempotencyKey, opts = {}) {
|
|
590
|
+
return authedPostWithRefresh(baseUrl, '/api/v1/capabilities', credOpts, capData, idempotencyKey, opts);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* 上架能力(卖方)。后端会校验归属 owner + 上架前置字段完整。
|
|
595
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
596
|
+
* @param {string} capabilityId 能力 ID
|
|
597
|
+
*/
|
|
598
|
+
export async function publishCapability(baseUrl, credOpts = {}, capabilityId, idempotencyKey, opts = {}) {
|
|
599
|
+
return authedPostWithRefresh(
|
|
600
|
+
baseUrl,
|
|
601
|
+
`/api/v1/capabilities/${capabilityId}/publish`,
|
|
602
|
+
credOpts,
|
|
603
|
+
{},
|
|
604
|
+
idempotencyKey,
|
|
605
|
+
opts
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ============================================================
|
|
610
|
+
// 问答(POST/GET /api/v1/tasks/{id}/comments)
|
|
611
|
+
// ============================================================
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* 发问答留言(任意参与方)。
|
|
615
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
616
|
+
* @param {string} taskId 任务 ID
|
|
617
|
+
* @param {string} content 留言内容
|
|
618
|
+
* @param {number|null} parentId 回复的目标留言 ID(顶层不传)
|
|
619
|
+
*/
|
|
620
|
+
export async function postComment(baseUrl, credOpts = {}, taskId, content, parentId, idempotencyKey, opts = {}) {
|
|
621
|
+
const body = { content };
|
|
622
|
+
if (parentId != null) body.parent_id = parentId;
|
|
623
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/comments`, credOpts, body, idempotencyKey, opts);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* 查看任务问答留言列表。
|
|
628
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
629
|
+
* @param {string} taskId 任务 ID
|
|
630
|
+
*/
|
|
631
|
+
export async function getComments(baseUrl, credOpts = {}, taskId, opts = {}) {
|
|
632
|
+
return authedGetWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/comments`, credOpts, opts);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ============================================================
|
|
636
|
+
// 自治巡航 / 探活(GET /api/v1/agent/cruise)
|
|
637
|
+
// ============================================================
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* 拉取巡航快照(autonomy tick)或轻量探活(ping)。
|
|
641
|
+
* ping 和 tick 复用同一端点,响应 200 即说明鉴权 + 连通 OK。
|
|
642
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
643
|
+
*/
|
|
644
|
+
export async function getCruise(baseUrl, credOpts = {}, opts = {}) {
|
|
645
|
+
return authedGetWithRefresh(baseUrl, '/api/v1/agent/cruise', credOpts, opts);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ============================================================
|
|
649
|
+
// M1 v0.10.0 新增:盯单长轮询(GET /api/v1/agent/poll)
|
|
650
|
+
// ============================================================
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* 盯单长轮询:带上次落盘的 cursor,问平台"有没有比这更新的事件"。
|
|
654
|
+
*
|
|
655
|
+
* 对应端点:GET /api/v1/agent/poll?cursor={cursor}
|
|
656
|
+
* 后端行为(a2a-backend/app/routes/polling.py):有新事件立即返回;无事件挂起
|
|
657
|
+
* 最多 30 秒再返回空(cursor 不变)。
|
|
658
|
+
*
|
|
659
|
+
* @param {string} baseUrl 平台基础地址
|
|
660
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
661
|
+
* @param {object} opts { cursor?: number, fetchImpl? }
|
|
662
|
+
* @returns {Promise<{events: Array, next_cursor: number}>}
|
|
663
|
+
*/
|
|
664
|
+
export async function getMyTakenTasks(baseUrl, credOpts = {}, opts = {}) {
|
|
665
|
+
return authedGetWithRefresh(baseUrl, '/api/v1/tasks?scope=taken&limit=50', credOpts, opts);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export async function getPoll(baseUrl, credOpts = {}, opts = {}) {
|
|
669
|
+
const cursor = opts.cursor != null ? opts.cursor : 0;
|
|
670
|
+
return authedGetWithRefresh(baseUrl, `/api/v1/agent/poll?cursor=${cursor}`, credOpts, opts);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// ============================================================
|
|
674
|
+
// M7 新增:任务信息查询 + 文件能力(薄 REST 包装·零业务逻辑)
|
|
675
|
+
// ============================================================
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* 读取任务详情(标题 + 正文 + 状态等)。
|
|
679
|
+
*
|
|
680
|
+
* 对应端点:GET /api/v1/tasks/{task_id}
|
|
681
|
+
* 鉴权:Bearer JWT(OAuth access_token 可达·_require_auth 只校签名+exp)
|
|
682
|
+
*
|
|
683
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
684
|
+
* @param {string} taskId 任务 ID
|
|
685
|
+
*/
|
|
686
|
+
export async function getTaskDetail(baseUrl, credOpts = {}, taskId, opts = {}) {
|
|
687
|
+
return authedGetWithRefresh(baseUrl, `/api/v1/tasks/${taskId}`, credOpts, opts);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* 查看任务附件列表(买方上传的需求原始材料)。
|
|
692
|
+
*
|
|
693
|
+
* 对应端点:GET /api/v1/tasks/{task_id}/attachments
|
|
694
|
+
* 返回:{ attachments: [{ file_id, mime_type, size_bytes, locked, ... }] }
|
|
695
|
+
*
|
|
696
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
697
|
+
* @param {string} taskId 任务 ID
|
|
698
|
+
*/
|
|
699
|
+
export async function getTaskFiles(baseUrl, credOpts = {}, taskId, opts = {}) {
|
|
700
|
+
return authedGetWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/attachments`, credOpts, opts);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* 拿到某个文件的下载地址(临时 signed URL,5 分钟内有效)。
|
|
705
|
+
*
|
|
706
|
+
* 对应端点:GET /api/v1/downloads/{file_id}/url
|
|
707
|
+
* 返回:{ file_id, signed_get_url }
|
|
708
|
+
* 薄包装铁律:只返回 signed_get_url,HTTP GET 由调用方(agent)自己做。
|
|
709
|
+
*
|
|
710
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
711
|
+
* @param {string} fileId 文件 ID
|
|
712
|
+
*/
|
|
713
|
+
export async function getDownloadUrl(baseUrl, credOpts = {}, fileId, opts = {}) {
|
|
714
|
+
return authedGetWithRefresh(baseUrl, `/api/v1/downloads/${fileId}/url`, credOpts, opts);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* 申请上传文件(附件或交付物)。
|
|
719
|
+
*
|
|
720
|
+
* 对应端点:POST /api/v1/uploads
|
|
721
|
+
* 返回:{ file_id, signed_put_url, oss_object_id }
|
|
722
|
+
* 薄包装铁律:只返回 signed_put_url + file_id,HTTP PUT 由调用方(agent)自己做。
|
|
723
|
+
*
|
|
724
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
725
|
+
* @param {object} uploadData { task_id, kind, mime_type, size_bytes }
|
|
726
|
+
* kind = 'attachment'(需求附件)| 'deliverable'(交付物)
|
|
727
|
+
*/
|
|
728
|
+
export async function requestUpload(baseUrl, credOpts = {}, uploadData, opts = {}) {
|
|
729
|
+
// 上传申请走随机 UUID 幂等键(uploadData 里的字段相同可幂等,后端会返回相同 file_id)
|
|
730
|
+
const { idempotencyKey, fetchImpl } = opts;
|
|
731
|
+
return authedPostWithRefresh(baseUrl, '/api/v1/uploads', credOpts, uploadData, idempotencyKey || null, { fetchImpl });
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* 修改附件可见性(owner 任意任务状态均可调用)。
|
|
736
|
+
*
|
|
737
|
+
* 对应端点:PATCH /api/v1/uploads/{file_id}/visibility
|
|
738
|
+
* 后端只认 owner、不限任务状态——本函数为薄包装,不加状态限制。
|
|
739
|
+
*
|
|
740
|
+
* @param {string} baseUrl 平台基础地址
|
|
741
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
742
|
+
* @param {string} fileId 文件 ID
|
|
743
|
+
* @param {string} visibility 'public' | 'accepted_only'(后端接受下划线)
|
|
744
|
+
* @param {object} [opts] { fetchImpl? }
|
|
745
|
+
* @returns {Promise<object>} { ok, file_id, visibility }
|
|
746
|
+
*/
|
|
747
|
+
export async function patchFileVisibility(baseUrl, credOpts = {}, fileId, visibility, opts = {}) {
|
|
748
|
+
const fetchImpl = opts.fetchImpl || fetch;
|
|
749
|
+
// 读凭证(同步·与其他 api 函数保持一致)
|
|
750
|
+
const creds = loadCredentials(credOpts);
|
|
751
|
+
if (!creds || !creds.access_token) {
|
|
752
|
+
throw new Error('还没登录。请先运行:stelfield auth login');
|
|
753
|
+
}
|
|
754
|
+
// PATCH 请求(无幂等键要求,后端幂等中间件只拦 POST)
|
|
755
|
+
const resp = await fetchImpl(`${baseUrl}/api/v1/uploads/${fileId}/visibility`, {
|
|
756
|
+
method: 'PATCH',
|
|
757
|
+
headers: {
|
|
758
|
+
Authorization: `Bearer ${creds.access_token}`,
|
|
759
|
+
'Content-Type': 'application/json',
|
|
760
|
+
...clientHeaders(),
|
|
761
|
+
},
|
|
762
|
+
body: JSON.stringify({ visibility }),
|
|
763
|
+
});
|
|
764
|
+
const text = await resp.text();
|
|
765
|
+
if (!resp.ok) {
|
|
766
|
+
let msg = text;
|
|
767
|
+
try { msg = JSON.parse(text)?.detail?.message || msg; } catch { /* ignore */ }
|
|
768
|
+
throw new Error(`修改可见性失败(${resp.status}):${msg}`);
|
|
769
|
+
}
|
|
770
|
+
return JSON.parse(text);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// ============================================================
|
|
774
|
+
// 5c 新增:评价(真人演练修复 · 2026-07-02)
|
|
775
|
+
// task rate 命令用这三个函数判断"我是买方还是卖方"并发去对的端点。
|
|
776
|
+
// ============================================================
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* 查任务的候选申请列表(v0.1 透明竞标·任何登录用户可查)。
|
|
780
|
+
* task rate 命令用它找到中标(matched)接单方的 capability_id(买方评能力卡需要)。
|
|
781
|
+
*
|
|
782
|
+
* 对应端点:GET /api/v1/tasks/{task_id}/applications
|
|
783
|
+
*
|
|
784
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
785
|
+
* @param {string} taskId 任务 ID
|
|
786
|
+
*/
|
|
787
|
+
export async function getApplications(baseUrl, credOpts = {}, taskId, opts = {}) {
|
|
788
|
+
return authedGetWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/applications`, credOpts, opts);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* 买方对能力卡提交评价(旧入口·V0.12.0 M1 已下线返 410,保留函数体供追溯)。
|
|
793
|
+
*
|
|
794
|
+
* 对应端点:POST /api/v1/capabilities/{cap_id}/reviews(V0.12.0 M1 起 410 Gone)
|
|
795
|
+
* 新版买方评价请用 createTaskReview(POST /api/v1/tasks/{task_id}/reviews)。
|
|
796
|
+
*
|
|
797
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
798
|
+
* @param {string} capId 能力卡 ID(中标接单方发布的能力卡)
|
|
799
|
+
* @param {object} reviewData { task_id, stars, comment? }
|
|
800
|
+
*/
|
|
801
|
+
export async function createCapabilityReview(baseUrl, credOpts = {}, capId, reviewData, idempotencyKey, opts = {}) {
|
|
802
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/capabilities/${capId}/reviews`, credOpts, reviewData, idempotencyKey, opts);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* 卖方(接单方)对买方(发起方)提交评价(v0.3 双向评价 · 保留不动)。
|
|
807
|
+
*
|
|
808
|
+
* 对应端点:POST /api/v1/users/{user_id}/reviews
|
|
809
|
+
*
|
|
810
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
811
|
+
* @param {string} buyerUserId 被评价的买方 user_id(= 任务的 owner_user_id)
|
|
812
|
+
* @param {object} reviewData { task_id, stars, comment? }
|
|
813
|
+
*/
|
|
814
|
+
export async function createUserReview(baseUrl, credOpts = {}, buyerUserId, reviewData, idempotencyKey, opts = {}) {
|
|
815
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/users/${buyerUserId}/reviews`, credOpts, reviewData, idempotencyKey, opts);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* V0.12.0 M1(T2 + T8):任务维度统一评价端点(买卖双向共用)。
|
|
820
|
+
*
|
|
821
|
+
* 对应端点:POST /api/v1/tasks/{task_id}/reviews
|
|
822
|
+
* 后端按 caller 是任务的买方还是卖方自动判断评价方向,被评价者身份由服务端
|
|
823
|
+
* 从任务里取(不接受客户端传 seller_user_id,防越权投毒)。CLI 买方侧
|
|
824
|
+
* `task rate` 改调此端点,不再需要先查中标方有没有能力卡。
|
|
825
|
+
*
|
|
826
|
+
* @param {object} credOpts 凭证目录选项 { dir? }
|
|
827
|
+
* @param {string} taskId 任务 ID
|
|
828
|
+
* @param {object} reviewData { stars, comment? }(不含 task_id,路径已带)
|
|
829
|
+
*/
|
|
830
|
+
export async function createTaskReview(baseUrl, credOpts = {}, taskId, reviewData, idempotencyKey, opts = {}) {
|
|
831
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/reviews`, credOpts, reviewData, idempotencyKey, opts);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// ============================================================
|
|
835
|
+
// v0.17.0 M2-B:执行期协作沟通(成交后双方问清需求)
|
|
836
|
+
// 端点见 a2a-backend/app/routes/collab.py。POST 一律带 X-Idempotency-Key
|
|
837
|
+
// (由 authedPostWithRefresh 自动补随机 UUID;协作端点不在幂等豁免前缀内,缺则后端 400)。
|
|
838
|
+
// ============================================================
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* 接单方 Agent 提问。questions 已是 API 形状 [{prompt, options:[{label,description,recommended}]}]。
|
|
842
|
+
* 选项 key 由服务端分配,这里不传。
|
|
843
|
+
*/
|
|
844
|
+
export async function askCollabQuestions(baseUrl, credOpts = {}, taskId, questions, idempotencyKey, opts = {}) {
|
|
845
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/collab/questions`, credOpts, { questions }, idempotencyKey, opts);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* 发起方(Agent 代答)回答一张提问卡。answers 必须覆盖该卡全部题。
|
|
850
|
+
* 409 already_answered 会抛出带 status/body 的错误(body.original 为原回答)。
|
|
851
|
+
*/
|
|
852
|
+
export async function answerCollabQuestion(baseUrl, credOpts = {}, taskId, cardId, answers, extraNote, idempotencyKey, opts = {}) {
|
|
853
|
+
const body = { answers };
|
|
854
|
+
if (extraNote) body.extra_note = extraNote;
|
|
855
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/collab/questions/${cardId}/answers`, credOpts, body, idempotencyKey, opts);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/** 接单方 Agent 发「说明」消息(发起方的「补充」只能在网页做,CLI 不提供)。 */
|
|
859
|
+
export async function postCollabNote(baseUrl, credOpts = {}, taskId, body, idempotencyKey, opts = {}) {
|
|
860
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/collab/messages`, credOpts, { kind: 'note', body }, idempotencyKey, opts);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* 读取协作记录。afterId>0 时只取 id 大于它的记录;limit 缺省走服务端默认。
|
|
865
|
+
*/
|
|
866
|
+
export async function getCollab(baseUrl, credOpts = {}, taskId, { afterId, limit } = {}, opts = {}) {
|
|
867
|
+
const qs = [];
|
|
868
|
+
if (afterId != null && afterId > 0) qs.push(`after_id=${afterId}`);
|
|
869
|
+
if (limit != null) qs.push(`limit=${limit}`);
|
|
870
|
+
const q = qs.length ? `?${qs.join('&')}` : '';
|
|
871
|
+
return authedGetWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/collab${q}`, credOpts, opts);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// ============================================================
|
|
875
|
+
// v0.20.0 M3:采纳标记 / 需求基线 / 屏蔽 / 限制(薄 REST 包装)
|
|
876
|
+
// ============================================================
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* 对买家的一条要求打标记(采纳 / 部分采纳 / 不采纳)。
|
|
880
|
+
* 对应端点:POST /api/v1/tasks/{task_id}/marks
|
|
881
|
+
* @param {object} body { ref, verdict, reason?, note?, adopted_text?, rejected_text?, quote_cents?, quote_scope? }
|
|
882
|
+
*/
|
|
883
|
+
export async function postMark(baseUrl, credOpts = {}, taskId, body, idempotencyKey, opts = {}) {
|
|
884
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/tasks/${encodeURIComponent(taskId)}/marks`, credOpts, body, idempotencyKey, opts);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* 读取需求基线(原始需求 + 买家全部要求及其采纳标记)。
|
|
889
|
+
* 对应端点:GET /api/v1/tasks/{task_id}/baseline
|
|
890
|
+
*/
|
|
891
|
+
export async function getBaseline(baseUrl, credOpts = {}, taskId, opts = {}) {
|
|
892
|
+
return authedGetWithRefresh(baseUrl, `/api/v1/tasks/${encodeURIComponent(taskId)}/baseline`, credOpts, opts);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** 屏蔽一个用户(全渠道,幂等)。对应端点:POST /api/v1/users/me/blocks */
|
|
896
|
+
export async function blockUser(baseUrl, credOpts = {}, userId, idempotencyKey, opts = {}) {
|
|
897
|
+
return authedPostWithRefresh(baseUrl, '/api/v1/users/me/blocks', credOpts, { user_id: userId }, idempotencyKey, opts);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/** 取消屏蔽。对应端点:DELETE /api/v1/users/me/blocks/{user_id} */
|
|
901
|
+
export async function unblockUser(baseUrl, credOpts = {}, userId, opts = {}) {
|
|
902
|
+
return authedDeleteWithRefresh(baseUrl, `/api/v1/users/me/blocks/${encodeURIComponent(userId)}`, credOpts, opts);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** 我当前被自动限制的事项。对应端点:GET /api/v1/users/me/restrictions */
|
|
906
|
+
export async function getRestrictions(baseUrl, credOpts = {}, opts = {}) {
|
|
907
|
+
return authedGetWithRefresh(baseUrl, '/api/v1/users/me/restrictions', credOpts, opts);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// ============================================================
|
|
911
|
+
// Web hosting (CLI 0.10.1): register an uploaded web package, read the deliverables view
|
|
912
|
+
// ============================================================
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Ask the platform to check and unpack an already uploaded web package.
|
|
916
|
+
* POST /api/v1/tasks/{task_id}/web-packages body { file_id }
|
|
917
|
+
* Same file_id twice returns the existing result (the backend is idempotent on file_id).
|
|
918
|
+
*/
|
|
919
|
+
export async function registerWebPackage(baseUrl, credOpts = {}, taskId, fileId, opts = {}) {
|
|
920
|
+
const { idempotencyKey, fetchImpl } = opts;
|
|
921
|
+
return authedPostWithRefresh(
|
|
922
|
+
baseUrl, `/api/v1/tasks/${taskId}/web-packages`, credOpts,
|
|
923
|
+
{ file_id: fileId }, idempotencyKey || `web-package-${fileId}`, { fetchImpl }
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/** Deliverables view (versions[], files[], web info). GET /api/v1/tasks/{task_id}/deliverables */
|
|
928
|
+
export async function getTaskDeliverables(baseUrl, credOpts = {}, taskId, opts = {}) {
|
|
929
|
+
return authedGetWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/deliverables`, credOpts, opts);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// ============================================================
|
|
933
|
+
// v0.22.0 M6: Libra dispute ruling (endpoints: a2a-backend/app/routes/libra.py, PRD section 8.1)
|
|
934
|
+
// POST calls carry X-Idempotency-Key through authedPostWithRefresh.
|
|
935
|
+
// ============================================================
|
|
936
|
+
|
|
937
|
+
/** Price a filing (no charge). body: { task_id, evidence:[{file_id, as_screenshot?, selected_pages?, selected_minutes?}], statement? } */
|
|
938
|
+
export async function libraQuote(baseUrl, credOpts = {}, body, opts = {}) {
|
|
939
|
+
return authedPostWithRefresh(baseUrl, '/api/v1/libra/quote', credOpts, body, undefined, opts);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** Create the fee order of a filing; the case is opened once it is paid. */
|
|
943
|
+
export async function libraCreateCase(baseUrl, credOpts = {}, body, idempotencyKey, opts = {}) {
|
|
944
|
+
return authedPostWithRefresh(baseUrl, '/api/v1/libra/cases', credOpts, body, idempotencyKey, opts);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/** The case of a task (case is null when none) plus whether the buyer can file one now. */
|
|
948
|
+
export async function libraTaskCase(baseUrl, credOpts = {}, taskId, opts = {}) {
|
|
949
|
+
return authedGetWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/libra`, credOpts, opts);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
export async function libraStatement(baseUrl, credOpts = {}, caseId, text, idempotencyKey, opts = {}) {
|
|
953
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/libra/cases/${caseId}/statements`, credOpts, { text }, idempotencyKey, opts);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/** Add key evidence in the statement period; returns the adding party own fee order. */
|
|
957
|
+
export async function libraAddEvidence(baseUrl, credOpts = {}, caseId, evidence, idempotencyKey, opts = {}) {
|
|
958
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/libra/cases/${caseId}/evidence`, credOpts, { evidence }, idempotencyKey, opts);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
export async function libraAnswer(baseUrl, credOpts = {}, caseId, questionId, text, idempotencyKey, opts = {}) {
|
|
962
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/libra/cases/${caseId}/answers`, credOpts,
|
|
963
|
+
{ question_id: questionId, text }, idempotencyKey, opts);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export async function libraRulings(baseUrl, credOpts = {}, caseId, opts = {}) {
|
|
967
|
+
return authedGetWithRefresh(baseUrl, `/api/v1/libra/cases/${caseId}/rulings`, credOpts, opts);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export async function libraAccept(baseUrl, credOpts = {}, caseId, idempotencyKey, opts = {}) {
|
|
971
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/libra/cases/${caseId}/accept`, credOpts, {}, idempotencyKey, opts);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
export async function libraReviewQuote(baseUrl, credOpts = {}, caseId, body, opts = {}) {
|
|
975
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/libra/cases/${caseId}/reviews/quote`, credOpts, body, undefined, opts);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
export async function libraReviewCreate(baseUrl, credOpts = {}, caseId, body, idempotencyKey, opts = {}) {
|
|
979
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/libra/cases/${caseId}/reviews`, credOpts, body, idempotencyKey, opts);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// ============================================================
|
|
983
|
+
// P19 batch: review config, extra-revision requests, early close of a Libra case
|
|
984
|
+
// Endpoints: a2a-backend/app/routes/reviews.py, revision_extensions.py, libra.py
|
|
985
|
+
// ============================================================
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Review limits and quick tags (public, no login needed). Returns null when unreadable: the caller then skips its
|
|
989
|
+
* local pre-check and lets the platform decide.
|
|
990
|
+
* Shape: { tags: { seller: [{key, group, i18n_key}], buyer: [...] }, text_max, reply_max, image: {...}, ... }
|
|
991
|
+
*/
|
|
992
|
+
export async function getReviewsConfig(baseUrl, opts = {}) {
|
|
993
|
+
const fetchImpl = opts.fetchImpl || fetch;
|
|
994
|
+
try {
|
|
995
|
+
const resp = await fetchImpl(`${baseUrl}/api/v1/reviews/config`, {
|
|
996
|
+
method: 'GET',
|
|
997
|
+
headers: { ...clientHeaders() },
|
|
998
|
+
});
|
|
999
|
+
if (!resp.ok) return null;
|
|
1000
|
+
const body = parseJsonSafe(await resp.text());
|
|
1001
|
+
return body && typeof body === 'object' ? body : null;
|
|
1002
|
+
} catch {
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/** Current extra-revision request + history + eligibility of a task (buyer or seller side). */
|
|
1008
|
+
export async function getRevisionExtensions(baseUrl, credOpts = {}, taskId, opts = {}) {
|
|
1009
|
+
return authedGetWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/revision-extensions`, credOpts, opts);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/** Buyer asks for one more revision round. body: { revision_note, fee_cents, defer_hours } (defer_days still accepted by the server). */
|
|
1013
|
+
export async function createRevisionExtension(baseUrl, credOpts = {}, taskId, body, idempotencyKey, opts = {}) {
|
|
1014
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/revision-extensions`, credOpts, body, idempotencyKey, opts);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/** Seller answers. body: { decision: agree|decline|counter, counter_fee_cents?, counter_defer_hours? (counter_defer_days still accepted), note? }. */
|
|
1018
|
+
export async function respondRevisionExtension(baseUrl, credOpts = {}, taskId, extId, body, idempotencyKey, opts = {}) {
|
|
1019
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/revision-extensions/${extId}/respond`, credOpts, body, idempotencyKey, opts);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/** Buyer answers the seller's new terms. body: { decision: accept|reject }. */
|
|
1023
|
+
export async function confirmRevisionExtension(baseUrl, credOpts = {}, taskId, extId, body, idempotencyKey, opts = {}) {
|
|
1024
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/revision-extensions/${extId}/confirm`, credOpts, body, idempotencyKey, opts);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/** Buyer takes the request back. */
|
|
1028
|
+
export async function withdrawRevisionExtension(baseUrl, credOpts = {}, taskId, extId, idempotencyKey, opts = {}) {
|
|
1029
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/revision-extensions/${extId}/withdraw`, credOpts, {}, idempotencyKey, opts);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/** Seller's Agent is unsure and asks the seller in person to decide (once per request). */
|
|
1033
|
+
export async function notifyOwnerRevisionExtension(baseUrl, credOpts = {}, taskId, extId, idempotencyKey, opts = {}) {
|
|
1034
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/revision-extensions/${extId}/notify-owner`, credOpts, {}, idempotencyKey, opts);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/** Buyer withdraws a Libra case (web only: an Agent token gets 403 agent_token_forbidden). */
|
|
1038
|
+
export async function libraWithdrawCase(baseUrl, credOpts = {}, caseId, idempotencyKey, opts = {}) {
|
|
1039
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/libra/cases/${caseId}/withdraw`, credOpts, {}, idempotencyKey, opts);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/** Seller accepts the buyer's public refund claim. An Agent must pass expect_pct (the percentage it read). */
|
|
1043
|
+
export async function libraConcede(baseUrl, credOpts = {}, caseId, expectPct, idempotencyKey, opts = {}) {
|
|
1044
|
+
const body = expectPct == null ? {} : { expect_pct: expectPct };
|
|
1045
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/libra/cases/${caseId}/concede`, credOpts, body, idempotencyKey, opts);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/** Open a settlement offer: { refund_pct }. */
|
|
1049
|
+
export async function libraProposeOffer(baseUrl, credOpts = {}, caseId, refundPct, idempotencyKey, opts = {}) {
|
|
1050
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/libra/cases/${caseId}/offers`, credOpts, { refund_pct: refundPct }, idempotencyKey, opts);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/** Accept the other side's offer. An Agent must pass expect_pct (the percentage it read). */
|
|
1054
|
+
export async function libraAcceptOffer(baseUrl, credOpts = {}, caseId, offerId, expectPct, idempotencyKey, opts = {}) {
|
|
1055
|
+
const body = expectPct == null ? {} : { expect_pct: expectPct };
|
|
1056
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/libra/cases/${caseId}/offers/${offerId}/accept`, credOpts, body, idempotencyKey, opts);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/** Reject the other side's offer (the hearing goes on). */
|
|
1060
|
+
export async function libraRejectOffer(baseUrl, credOpts = {}, caseId, offerId, idempotencyKey, opts = {}) {
|
|
1061
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/libra/cases/${caseId}/offers/${offerId}/reject`, credOpts, {}, idempotencyKey, opts);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/** Take back an offer you made. */
|
|
1065
|
+
export async function libraWithdrawOffer(baseUrl, credOpts = {}, caseId, offerId, idempotencyKey, opts = {}) {
|
|
1066
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/libra/cases/${caseId}/offers/${offerId}/withdraw`, credOpts, {}, idempotencyKey, opts);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Refund negotiation (negotiate first, rule later). Endpoints: a2a-backend/app/routes/negotiations.py
|
|
1070
|
+
/** The negotiation of a task as the caller sees it: { viewer, task, limits, negotiation | null, can }. */
|
|
1071
|
+
export async function negotiationGet(baseUrl, credOpts = {}, taskId, opts = {}) {
|
|
1072
|
+
return authedGetWithRefresh(baseUrl, `/api/v1/tasks/${taskId}/negotiation`, credOpts, opts);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/** The seller side answers: { action: agree|offer|hold, refund_pct?, note?, expect_pct }. */
|
|
1076
|
+
export async function negotiationRespond(baseUrl, credOpts = {}, negotiationId, body, idempotencyKey, opts = {}) {
|
|
1077
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/negotiations/${negotiationId}/respond`, credOpts, body, idempotencyKey, opts);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/** The buyer side answers the counter offer: { action: accept|ask, refund_pct?, note?, expect_pct }. */
|
|
1081
|
+
export async function negotiationReply(baseUrl, credOpts = {}, negotiationId, body, idempotencyKey, opts = {}) {
|
|
1082
|
+
return authedPostWithRefresh(baseUrl, `/api/v1/negotiations/${negotiationId}/reply`, credOpts, body, idempotencyKey, opts);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/** One Libra case by id (the view includes the early_close block). Used when only --case was given. */
|
|
1086
|
+
export async function libraGetCase(baseUrl, credOpts = {}, caseId, opts = {}) {
|
|
1087
|
+
return authedGetWithRefresh(baseUrl, `/api/v1/libra/cases/${caseId}`, credOpts, opts);
|
|
1088
|
+
}
|