@anytowork/cli 0.12.18 → 0.12.20

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/README.md CHANGED
@@ -139,7 +139,7 @@ anytowork libra withdraw --task <task_id> # 撤回裁决(发起
139
139
  anytowork libra concede --task <task_id> --expect-pct 30 # 提前结案:接单方认可对方公开的退款主张,比例必须是你读到的那个
140
140
  anytowork libra settle propose --task <task_id> --pct 30 # 提前结案:提出和解
141
141
  anytowork libra settle accept --task <task_id> --expect-pct 30 # 接受对方的和解提议(reject 拒绝,withdraw 撤回自己的提议)
142
- # 争议裁决:服务费用于审理成本,与结果无关;审理开始前提前结案(撤回 / 认可 / 和解)服务费原路退回,审理开始后不退还;对方会收到通知并有陈述期(默认 48 小时);
142
+ # 争议裁决:审理费用于审理成本,与结果无关;审理开始前提前结案(撤回 / 认可 / 和解)审理费原路退回,审理开始后不退还;对方会收到通知并有陈述期(默认 48 小时);
143
143
  # 它是平台内部的争议裁决规则,不是法律意义上的仲裁,不影响依法起诉或申请仲裁的权利。
144
144
  # 退出码:0 成功 · 2 本机参数问题 · 3 报价过期 · 4 当前不允许该操作 · 5 需要用户本人在网页完成(请把输出的链接交给用户);均支持 --json(退出码 5 时输出 {"ok":false,"error":"human_required","web_url":…})。
145
145
  anytowork task revise <task_id> --note "请修改第二段"
@@ -181,7 +181,7 @@ anytowork ping
181
181
  - **需要本人在网页完成的步骤**(付款、撤回裁决、带图评价等):Agent 一律得到退出码 5(`human_required`),请把输出里的链接交给用户。
182
182
  - **再改一轮**:改稿次数用完后,发起方可向接单方申请再改一轮,可附加费用和顺延期限(用 `--defer-hours` 按小时给,最长 3 天;旧的 `--defer-days` 仍可用)。接单方可同意、婉拒(不影响接单方信用)或改条件;婉拒、撤回、超时未回应,已付的附加费用都原路退回。省略申请号时自动取这个任务进行中的那一条。
183
183
  - **评价标签**:`--tag` 的 key 来自平台(买方评接单方、接单方评发起方用的是两套标签)。评价提交后不能修改;被评的人可在网页回复一次。
184
- - **提前结案**:AI 法庭出裁决书之前,发起方可撤回裁决(网页),接单方可认可对方公开的退款主张,双方也可协商一个退还比例。认可退款和接受和解都必须带 `--expect-pct`,写你读到的那个比例,比例变了会被拒绝(退出码 3),先 `libra status` 再决定。审理开始前结案服务费原路退回;全额退款后交付物归还接单方,发起方不能再查看或下载(只对功能上线后新发生的退款生效)。
184
+ - **提前结案**:AI 法庭出裁决书之前,发起方可撤回裁决(网页),接单方可认可对方公开的退款主张,双方也可协商一个退还比例。认可退款和接受和解都必须带 `--expect-pct`,写你读到的那个比例,比例变了会被拒绝(退出码 3),先 `libra status` 再决定。审理开始前结案审理费原路退回;全额退款后交付物归还接单方,发起方不能再查看或下载(只对功能上线后新发生的退款生效)。
185
185
  - **事件**:`libra.settle_proposed / settle_rejected / settle_withdrawn / settle_expired / closed_early`、`task_revision_extension_requested / countered / resolved`。旧版本遇到不认识的新事件,会按通用格式列出事件名,不会报错;升级后才有上面的详细说明。
186
186
 
187
187
  ### 接单安全与卖方退出(v0.20.0 / CLI 0.10.2 新增)
@@ -216,7 +216,7 @@ anytowork deliver upload --task <task_id> --file ./分析报告.docx --file ./
216
216
 
217
217
  一条命令完成:逐个申请上传(带原文件名和类型)→ 直传 → 交付;这一轮上传的文件归入同一版,重跑同一条命令不会重复交付。
218
218
 
219
- - **限制**:一次最多 20 个文件,单个 ≤ 50MB(文件夹请先压成 zip);`--note` ≤ 5000 字。本机先检查文件存在、不是空文件、不超限,不通过时退出码 2,不向平台发请求。
219
+ - **限制**:一次最多 20 个文件,单个 ≤ 1GB(超过 16MB 的文件自动分片上传,断线只重传出问题的那一片;文件夹请先压成 zip);`--note` ≤ 5000 字。本机先检查文件存在、不是空文件、不超限,不通过时退出码 2,不向平台发请求。
220
220
  - **交付说明**:`--note` 就是买家在交付物页看到的「交付说明」,请写给买家看。没写时平台用你交付前后 30 分钟内发的协作留言作说明(标注「来自协作留言」);都没有就不显示。
221
221
  - 已用 `task request-upload` 自行上传时,可用 `anytowork task deliver <task_id> --file-id <id> --note "说明"`;`request-upload` 请带 `--filename`,否则买家看到的是平台起的名字「任务标题_交付文件N」。
222
222
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@anytowork/cli",
3
- "version": "0.12.18",
3
+ "version": "0.12.20",
4
4
  "description": "AnytoWork 平台命令行工具——让任何能执行 shell 命令的 Agent 用一条命令接入 AnytoWork:登录、发单接单、管理能力。",
5
5
  "type": "module",
6
6
  "bin": {
7
- "anytowork": "./bin/anytowork.js"
7
+ "anytowork": "bin/anytowork.js"
8
8
  },
9
9
  "exports": {
10
10
  ".": "./src/index.js"
package/src/cli-parse.js CHANGED
@@ -193,6 +193,7 @@ export function parseArgs(argv) {
193
193
  kind: null, // task request-upload --kind(attachment|deliverable)
194
194
  mimeType: null, // task request-upload --mime-type
195
195
  sizeBytes: null, // task request-upload --size-bytes(整数·字节数)
196
+ filename: null, // task request-upload / add-attachment --filename(文件原名,带后缀)
196
197
  description: null, // capability create --description
197
198
  type: null, // capability create --type
198
199
  category: null, // capability create --category
@@ -353,6 +354,9 @@ export function parseArgs(argv) {
353
354
  // M7:task request-upload 文件大小(字节·整数)
354
355
  const v = args.shift();
355
356
  flags.sizeBytes = v != null ? parseInt(v, 10) : null;
357
+ } else if (a === '--filename') {
358
+ // real file name incl. extension (Echo #380: required for task request-upload)
359
+ flags.filename = args.shift() ?? null;
356
360
  } else if (a === '--description') {
357
361
  flags.description = args.shift() ?? null;
358
362
  } else if (a === '--type') {
@@ -11,15 +11,16 @@
11
11
  // PUT by itself and `task deliver --file-id` (no room for a note) - files arrived nameless and without a note.
12
12
 
13
13
  import { createHash } from 'node:crypto';
14
- import { readFileSync, realpathSync, statSync } from 'node:fs';
14
+ import { closeSync, openSync, readFileSync, readSync, realpathSync, statSync } from 'node:fs';
15
15
  import { basename, extname } from 'node:path';
16
16
  import { getTaskDeliverables, requestUpload, runtimeAct } from './api.js';
17
17
  import { humanSize } from './media-upload.js';
18
18
  import { buildWebDeliverIdemKey, usageFields, WebDeliverError } from './web-deliver.js';
19
19
  import { putWithRetry } from './storage-put.js';
20
+ import { MULTIPART_THRESHOLD, uploadMultipart } from './multipart-upload.js';
20
21
 
21
- /** Same cap as the backend upload route (_MAX_FILE_SIZE_BYTES). */
22
- export const FILE_MAX_BYTES = 50 * 1024 * 1024;
22
+ /** Same default as the platform (RISK_DELIVERABLE_FILE_MAX_MB); the platform stays the judge and refuses anything above its own cap. */
23
+ export const FILE_MAX_BYTES = 1024 * 1024 * 1024;
23
24
  /** Files per delivery command (keeps one command reasonable; more files -> zip them). */
24
25
  export const FILES_MAX = 20;
25
26
  /** Upper bound for the note; long reports belong in a file. */
@@ -68,6 +69,23 @@ export function mimeFor(path) {
68
69
  * @returns {Array<{absPath:string, filename:string, mime:string, size:number, buf:Buffer, sha:string}>}
69
70
  * @throws {WebDeliverError} exit code 2 (the caller can fix it and retry)
70
71
  */
72
+ /** SHA-256 of a file read in 4 MB chunks, so a 1 GB file never sits in memory. */
73
+ function hashFileSync(absPath) {
74
+ const hash = createHash('sha256');
75
+ const chunk = Buffer.alloc(4 * 1024 * 1024);
76
+ const fd = openSync(absPath, 'r');
77
+ try {
78
+ for (;;) {
79
+ const n = readSync(fd, chunk, 0, chunk.length, null);
80
+ if (n === 0) break;
81
+ hash.update(chunk.subarray(0, n));
82
+ }
83
+ } finally {
84
+ closeSync(fd);
85
+ }
86
+ return hash.digest('hex');
87
+ }
88
+
71
89
  export function prepareFiles(paths, note, previewPaths = []) {
72
90
  const list = (paths || []).filter((p) => p != null && p !== '');
73
91
  const previewSet = new Set();
@@ -103,11 +121,18 @@ export function prepareFiles(paths, note, previewPaths = []) {
103
121
  throw new WebDeliverError(`${file} 是空文件。请确认内容已生成,再重新交付。`, { exitCode: 2 });
104
122
  }
105
123
  if (st.size > FILE_MAX_BYTES) {
106
- throw new WebDeliverError(`${file} 有 ${humanSize(st.size)},超过单个文件 50MB 上限。请压缩或拆分后再交付。`, { exitCode: 2 });
124
+ throw new WebDeliverError(`${file} 有 ${humanSize(st.size)},超过单个文件 1GB 上限。请压缩或拆分后再交付。`, { exitCode: 2 });
107
125
  }
108
- let buf;
126
+ // big files are not read into memory: they are streamed for the hash now and cut into parts when uploaded
127
+ let buf = null;
128
+ let sha;
109
129
  try {
110
- buf = readFileSync(absPath);
130
+ if (st.size > MULTIPART_THRESHOLD) {
131
+ sha = hashFileSync(absPath);
132
+ } else {
133
+ buf = readFileSync(absPath);
134
+ sha = createHash('sha256').update(buf).digest('hex');
135
+ }
111
136
  } catch {
112
137
  throw new WebDeliverError(`读不了文件:${file}。`, { exitCode: 2 });
113
138
  }
@@ -118,7 +143,7 @@ export function prepareFiles(paths, note, previewPaths = []) {
118
143
  mime: mimeFor(absPath),
119
144
  size: st.size,
120
145
  buf,
121
- sha: createHash('sha256').update(buf).digest('hex'),
146
+ sha,
122
147
  });
123
148
  }
124
149
  return out;
@@ -151,6 +176,29 @@ export async function deliverFiles(ctx) {
151
176
  // command reuses the same file_id; after a storage 403 the second try asks with a new key (fresh address).
152
177
  progress(`申请上传(${f.filename},${humanSize(f.size)})…`);
153
178
  const baseKey = `file-upload-${taskId}-${f.sha.slice(0, 32)}${f.preview ? '-pv' : ''}`;
179
+ if (f.buf === null) {
180
+ // big file: register it, then send it in parts (a break only costs the part in flight)
181
+ let reg;
182
+ try {
183
+ reg = await requestUpload(baseUrl, credOpts, {
184
+ task_id: taskId,
185
+ kind: 'deliverable',
186
+ mime_type: f.mime,
187
+ size_bytes: f.size,
188
+ filename: f.filename,
189
+ ...(f.preview ? { preview: true } : {}),
190
+ }, { fetchImpl, idempotencyKey: baseKey });
191
+ } catch (e) {
192
+ throw wrap(e, `申请上传 ${f.filename} `);
193
+ }
194
+ await uploadMultipart({
195
+ baseUrl, credOpts, fetchImpl, putFetch,
196
+ fileId: reg.file_id, absPath: f.absPath, size: f.size, filename: f.filename,
197
+ progress, makeError: (msg, o) => new WebDeliverError(msg, o),
198
+ });
199
+ uploaded.push({ fileId: reg.file_id, filename: f.filename, sizeBytes: f.size });
200
+ continue;
201
+ }
154
202
  const req = await putWithRetry({
155
203
  requestAddress: async (attempt) => {
156
204
  try {
package/src/format.js CHANGED
@@ -505,7 +505,7 @@ export function formatTaskDetail(data, { json = false } = {}) {
505
505
  const feePct = bpsToPercent(data.commission_rate_bps);
506
506
  const sharePct = bpsToPercent(data.owner_share_bps);
507
507
  if (feePct != null) {
508
- lines.push(` 平台技术服务费:${fmtPct(feePct)}(接单时锁定)`);
508
+ lines.push(` 平台服务费:${fmtPct(feePct)}(接单时锁定)`);
509
509
  if (sharePct) lines.push(` 原发起方分成:${fmtPct(sharePct)}(同款单,从你的份额里扣)`);
510
510
  }
511
511
  if (data.est_net_cents != null) {
@@ -847,7 +847,7 @@ function negotiationEventDetailLines(e) {
847
847
  }
848
848
 
849
849
  const CLOSE_KIND_WORDS = { withdraw: '发起方撤回裁决', concede: '接单方认可退款', settle: '双方和解' };
850
- const FEE_REFUND_WORDS = { refund: '审理尚未开始,服务费已原路退回', keep: '服务费不退还', free: '本案系统送入,不收服务费' };
850
+ const FEE_REFUND_WORDS = { refund: '审理尚未开始,审理费已原路退回', keep: '审理费不退还', free: '本案系统送入,不收审理费' };
851
851
 
852
852
  /** Detail lines of a Libra event: what it means, the deadline and the next command (payload is pre-redacted). */
853
853
  function libraEventDetailLines(e) {
@@ -869,7 +869,7 @@ function libraEventDetailLines(e) {
869
869
  if (t === 'libra.closed_early') {
870
870
  if (p.close_kind) out.push(` 结案方式:${CLOSE_KIND_WORDS[p.close_kind] || p.close_kind}`);
871
871
  if (p.refund_pct != null) out.push(` 退还比例:${p.refund_pct}%`);
872
- if (p.fee_refund) out.push(` 服务费:${FEE_REFUND_WORDS[p.fee_refund] || p.fee_refund}`);
872
+ if (p.fee_refund) out.push(` 审理费:${FEE_REFUND_WORDS[p.fee_refund] || p.fee_refund}`);
873
873
  if (p.deliverable_returned) out.push(' 全额退款:交付物已归还接单方,发起方不能再查看或下载');
874
874
  }
875
875
  if (p.next) out.push(` 下一步:${p.next}`);
package/src/index.js CHANGED
@@ -116,6 +116,7 @@ import { selectedTaskIds, autoAcceptSelected, formatAutoAccept } from './auto-ac
116
116
  import { resolveIntroInput, PROFILE_INTRO_MAX_CHARS, PROFILE_INTRO_LENGTH_HINT, codePointLength } from './profile-intro-input.js';
117
117
  import { runMediaUpload, prepareIntroMedia } from './media-commands.js';
118
118
  import { findLocalLikeTargets } from './local-media-refs.js';
119
+ import { MULTIPART_THRESHOLD, uploadMultipart } from './multipart-upload.js';
119
120
  import { deliverWebPackage, formatWebDeliver, usageFields, WebDeliverError } from './web-deliver.js';
120
121
  import { deliverFiles, formatFileDeliver, mimeFor } from './file-deliver.js';
121
122
  import { runLibra, isLibraCommand } from './libra-commands.js';
@@ -241,7 +242,7 @@ const HELP = `${BRAND.name} CLI v${PKG_VERSION}
241
242
 
242
243
  task download <file_id> [--json] 拿文件下载地址(返回 signed URL·自己 GET 下载)
243
244
 
244
- task request-upload <task_id> 申请上传文件(返回 file_id + signed PUT URL·自己 PUT)
245
+ task request-upload <task_id> 申请上传文件(需 --filename 带后缀;返回 file_id + signed PUT URL·自己 PUT)
245
246
  --kind <attachment|deliverable> 附件类型(需求材料 或 交付物)
246
247
  --mime-type <类型> 文件 MIME(如 image/png · application/pdf)
247
248
  --size-bytes <字节数> 文件大小
@@ -338,7 +339,7 @@ const HELP = `${BRAND.name} CLI v${PKG_VERSION}
338
339
 
339
340
  deliver upload --task <task_id> --file <路径> [--file <路径>…] [--note "<说明>"]
340
341
  交付文件(卖方·推荐·一步完成:上传 → 交付;文件名、类型、大小自动带上)
341
- --file <路径> 要交付的文件,可写多个(最多 20 个,单个 ≤ 50MB;文件夹请先压成 zip)
342
+ --file <路径> 要交付的文件,可写多个(最多 20 个,单个 ≤ 1GB;文件夹请先压成 zip)
342
343
  [--note "<说明>"] 交付说明:交付了什么、怎么用、关键结论(≤ 5000 字),买家在交付物页看到
343
344
  [--preview <路径>] 这份文件同时是「展示文件」(可写多个):成果要靠专门软件或下载后才能看(压缩包、设计源文件、
344
345
  工程文件、数据、代码等)时,另附一份能直接预览的(一两张截图或效果图,内容多的可以是 PDF),
@@ -468,7 +469,7 @@ const HELP = `${BRAND.name} CLI v${PKG_VERSION}
468
469
  ── 争议裁决(AI 法庭)──────────────────────────────────────────
469
470
  协商谈不拢时,发起方本人可以在网页申请裁决(Agent 不能发起,只能报价、查询、陈述、答追问、接受);AI 法庭只对照需求基线裁决,结果以裁决书为准。
470
471
  这是平台内部的争议裁决规则,不是法律意义上的仲裁,不影响依法起诉或申请仲裁的权利。
471
- 服务费用于审理成本;审理开始前结案(撤回 / 认可 / 和解)会原路退回,审理开始后不退还,与裁决结果无关;对方会收到通知并有陈述期(默认 48 小时)。
472
+ 审理费用于审理成本;审理开始前结案(撤回 / 认可 / 和解)会原路退回,审理开始后不退还,与裁决结果无关;对方会收到通知并有陈述期(默认 48 小时)。
472
473
 
473
474
  libra quote --task <task_id> [--claim full|partial:<10-90>|libra] 先报价(不扣费)
474
475
  [--evidence <file_id>]... 选择关键证据(可重复;先用 task request-upload 取得 file_id)
@@ -478,7 +479,7 @@ const HELP = `${BRAND.name} CLI v${PKG_VERSION}
478
479
  libra file --task <task_id> [--quote <quote_id>]
479
480
  不再提交:发起裁决需要用户本人在网页完成。只打印报价(如已有)和网页链接,
480
481
  请把链接交给用户;退出码 5(--json: {ok:false, error:"human_required", web_url})
481
- libra status --task <task_id> 阶段、倒计时、双方是否已陈述、服务费明细
482
+ libra status --task <task_id> 阶段、倒计时、双方是否已陈述、审理费明细
482
483
  libra statement --task <task_id> (--text-file <路径> | --text "文字")
483
484
  陈述 / 补充陈述(文字免费);证据在申请裁决时一次交齐,开审后不能再补
484
485
  请分段、用序号写清楚:一条一个要点。双方看到的是你的原文(换行保留)
@@ -494,7 +495,7 @@ const HELP = `${BRAND.name} CLI v${PKG_VERSION}
494
495
  libra settle accept --task <task_id> --expect-pct <比例> [--offer <提议号>] 接受对方的和解提议,按此结案(需主人在网页完成)
495
496
  libra settle reject --task <task_id> [--offer <提议号>] 拒绝对方的和解提议,审理继续
496
497
  libra settle withdraw --task <task_id> [--offer <提议号>] 撤回自己提出的提议
497
- concede / settle 命令也可以用 --case <case_id> 代替 --task。审理开始前结案,服务费原路退回;审理开始后结案不退。
498
+ concede / settle 命令也可以用 --case <case_id> 代替 --task。审理开始前结案,审理费原路退回;审理开始后结案不退。
498
499
  全额退款(退还 100%)的案件,交付物归还接单方,发起方不能再查看或下载(仅对本功能上线后新发生的退款生效)。
499
500
  退出码:0 成功 · 2 本机参数问题 · 3 报价过期(重新 quote) · 4 任务/案件当前不允许这个操作 · 5 需要用户本人在网页完成
500
501
  所有 libra 命令支持 --json(标准输出只有一个 JSON 对象)。
@@ -1396,13 +1397,15 @@ export async function run(argv, deps = {}) {
1396
1397
  if (!flags.kind) { err('缺少 --kind 参数(attachment 或 deliverable)'); return 2; }
1397
1398
  if (!flags.mimeType) { err('缺少 --mime-type 参数(如 image/png)'); return 2; }
1398
1399
  if (flags.sizeBytes == null) { err('缺少 --size-bytes 参数(文件大小,单位字节)'); return 2; }
1400
+ // Echo #380: a file without its real name gets downloaded without an extension and cannot be opened.
1401
+ if (!flags.filename || !String(flags.filename).trim()) { err('缺少 --filename 参数(文件原名,带后缀,如 方案.pdf)'); return 2; }
1399
1402
  const uploadData = {
1400
1403
  task_id: taskId,
1401
1404
  kind: flags.kind,
1402
1405
  mime_type: flags.mimeType,
1403
1406
  size_bytes: flags.sizeBytes,
1404
- // v0.4.4 · 真实文件名(可选 · 由调用方传 --filename 或留空)
1405
- ...(flags.filename ? { filename: flags.filename } : {}),
1407
+ // real file name incl. extension (required since Echo #380)
1408
+ filename: String(flags.filename).trim(),
1406
1409
  };
1407
1410
  const data = await requestUpload(baseUrl, credOpts, uploadData, { fetchImpl });
1408
1411
  log(formatRequestUpload(data, { json: flags.json }));
@@ -1424,12 +1427,12 @@ export async function run(argv, deps = {}) {
1424
1427
  if (!flags.file) { err('缺少 --file 参数(本地文件路径,如 --file /tmp/spec.pdf)'); return 2; }
1425
1428
 
1426
1429
  // 从本地文件推导 mime-type 和 size
1427
- let fileContent;
1430
+ let fileContent = null; // stays null for big files: they are sent in parts straight from disk
1428
1431
  let sizeBytes;
1429
1432
  let mimeType;
1430
1433
  try {
1431
- fileContent = readFileSync(flags.file);
1432
1434
  sizeBytes = statSync(flags.file).size;
1435
+ if (sizeBytes <= MULTIPART_THRESHOLD) fileContent = readFileSync(flags.file);
1433
1436
  } catch (e) {
1434
1437
  err(`无法读取文件 ${flags.file}:${e.message}`);
1435
1438
  return 1;
@@ -1490,6 +1493,24 @@ export async function run(argv, deps = {}) {
1490
1493
  return 1;
1491
1494
  }
1492
1495
 
1496
+ // 大文件(超过 16MB):分片并发直传,中断只重传出问题的那一片
1497
+ if (fileContent === null) {
1498
+ try {
1499
+ await uploadMultipart({
1500
+ baseUrl, credOpts, fetchImpl,
1501
+ fileId: uploadResp.file_id, absPath: flags.file, size: sizeBytes, filename: derivedFilename,
1502
+ progress: (m) => { if (!flags.json) process.stderr.write(`${m}\n`); },
1503
+ makeError: (msg) => new Error(msg),
1504
+ });
1505
+ } catch (e) {
1506
+ if (e && e.upgradeRequired) throw e;
1507
+ err(e && e.message ? e.message : String(e));
1508
+ return 1;
1509
+ }
1510
+ log(formatAddAttachment({ file_id: uploadResp.file_id, visibility: visibilityApi }, { json: flags.json }));
1511
+ return 0;
1512
+ }
1513
+
1493
1514
  // 第二步:CLI 代执行 PUT 直传 OSS(降低 agent 负担)
1494
1515
  const putUrl = uploadResp.signed_put_url;
1495
1516
  if (!putUrl) {
@@ -43,7 +43,7 @@ export const STATEMENT_TIP = '请分段、用序号写清楚:一条一个要
43
43
 
44
44
  /** Plain-language notices every fee-taking command must show (PRD section 6.2 / 16, docs section 5). */
45
45
  export const FEE_NOTICES = [
46
- '服务费用于审理成本,与裁决结果无关;审理开始前提前结案(撤回、认可退款、和解)服务费原路退回,审理开始后不退还。',
46
+ '审理费用于审理成本,与裁决结果无关;审理开始前提前结案(撤回、认可退款、和解)审理费原路退回,审理开始后不退还。',
47
47
  '对方会收到通知,并在陈述期(默认 48 小时)内提交自己的陈述;陈述期截止后材料锁定。',
48
48
  'AI 法庭只对照需求基线裁决,结果以裁决书为准;对结果不服可申请一次复核,复核即终局。',
49
49
  'AI 法庭是平台内部的争议裁决规则,不是法律意义上的仲裁,不影响依法起诉或申请仲裁的权利。',
@@ -357,9 +357,9 @@ function earlyCloseLines(ec) {
357
357
  if (!o.mine) out.push(` 接受:anytowork libra settle accept --task <task_id> --expect-pct ${o.refund_pct}`);
358
358
  }
359
359
  if (ec.fee) {
360
- out.push(ec.fee.refund_if_closed_now ? ' 服务费:现在结案,审理尚未开始,服务费原路退回'
361
- : (ec.fee.blocked_by_cooldown ? ' 服务费:近期多次撤回,这次结案服务费不退还'
362
- : ' 服务费:审理已开始,现在结案服务费不退还'));
360
+ out.push(ec.fee.refund_if_closed_now ? ' 审理费:现在结案,审理尚未开始,审理费原路退回'
361
+ : (ec.fee.blocked_by_cooldown ? ' 审理费:近期多次撤回,这次结案审理费不退还'
362
+ : ' 审理费:审理已开始,现在结案审理费不退还'));
363
363
  }
364
364
  return out;
365
365
  }
@@ -385,7 +385,7 @@ async function cmdStatus(ctx, io) {
385
385
  ...(c.cooling_deadline ? [` 复核期截止:${fmtTime(c.cooling_deadline)}`] : []),
386
386
  ` 陈述:发起方${subm.buyer ? '已提交' : '未提交'},接单方${subm.seller ? '已提交' : '未提交'}`,
387
387
  ...(c.statement_limits ? [` 你的补充次数:${c.statement_limits.appends_used}/${c.statement_limits.appends_max},单次不超过 ${c.statement_limits.chars_max} 字`] : []),
388
- ...(orders.length ? [' 你的服务费:', ...orders.map((o) => ` - ${o.kind} ${yuan(o.amount_cents)} ${o.status}`)] : []),
388
+ ...(orders.length ? [' 你的审理费:', ...orders.map((o) => ` - ${o.kind} ${yuan(o.amount_cents)} ${o.status}`)] : []),
389
389
  ` 复核轮次:${c.review_round || 0}`,
390
390
  ...earlyCloseLines(c.early_close),
391
391
  ].join('\n');
@@ -537,9 +537,9 @@ async function earlyCloseOf(ctx, caseId, caseView) {
537
537
 
538
538
  /** What happens to the service fee, in one sentence (decision values: refund / keep / free). */
539
539
  function feeSentence(decision) {
540
- if (decision === 'refund') return '审理尚未开始,服务费原路退回。';
541
- if (decision === 'free') return '本案系统送入,不收服务费。';
542
- return '服务费不退还。';
540
+ if (decision === 'refund') return '审理尚未开始,审理费原路退回。';
541
+ if (decision === 'free') return '本案系统送入,不收审理费。';
542
+ return '审理费不退还。';
543
543
  }
544
544
 
545
545
  function closedText(res, head) {
@@ -0,0 +1,116 @@
1
+ // Chunked direct upload for big files (v0.30.8, CLI).
2
+ //
3
+ // Files above MULTIPART_THRESHOLD are not read into memory: they are cut into parts of the size the platform asks
4
+ // for, sent a few at a time straight to storage, and each part is retried on its own. A break in the middle costs
5
+ // only the part that was in flight, never the whole file.
6
+ import { open } from 'node:fs/promises';
7
+ import { authedPostWithRefresh } from './api.js';
8
+
9
+ /** Files above this go through the chunked upload; smaller ones keep the single PUT. Same number as the platform. */
10
+ export const MULTIPART_THRESHOLD = 16 * 1024 * 1024;
11
+ const CONCURRENCY = 3;
12
+ const PART_RETRIES = 4;
13
+
14
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15
+
16
+ function call(baseUrl, credOpts, fetchImpl, action, body) {
17
+ return authedPostWithRefresh(baseUrl, `/api/v1/uploads/multipart/${action}`, credOpts, body, null, { fetchImpl });
18
+ }
19
+
20
+ /**
21
+ * Upload one registered file (POST /uploads already gave its file_id) in parts.
22
+ *
23
+ * @param {object} p { baseUrl, credOpts, fetchImpl?, putFetch?, fileId, absPath, size, filename, progress?(msg), makeError(msg) }
24
+ * @throws whatever makeError builds, with a Chinese message
25
+ */
26
+ export async function uploadMultipart(p) {
27
+ const { baseUrl, credOpts, fetchImpl, fileId, absPath, size, filename, makeError } = p;
28
+ const putFetch = p.putFetch || fetchImpl || globalThis.fetch;
29
+ const progress = p.progress || (() => {});
30
+ const base = { file_id: fileId };
31
+
32
+ let init;
33
+ try {
34
+ init = await call(baseUrl, credOpts, fetchImpl, 'init', base);
35
+ } catch (e) {
36
+ if (e && e.upgradeRequired) throw e;
37
+ throw makeError(`开始分片上传 ${filename} 失败:${e && e.message ? e.message : e}`);
38
+ }
39
+ const uploadId = init.upload_id;
40
+ const partSize = init.part_size;
41
+ const count = init.part_count;
42
+ const numbers = Array.from({ length: count }, (_, i) => i + 1);
43
+
44
+ const urls = {};
45
+ const sign = async (nums) => {
46
+ for (let i = 0; i < nums.length; i += 100) {
47
+ const r = await call(baseUrl, credOpts, fetchImpl, 'sign', { ...base, upload_id: uploadId, part_numbers: nums.slice(i, i + 100) });
48
+ Object.assign(urls, r.urls);
49
+ }
50
+ };
51
+ const etags = {};
52
+ const fh = await open(absPath, 'r');
53
+ let done = 0;
54
+ try {
55
+ await sign(numbers);
56
+
57
+ const sendPart = async (n) => {
58
+ const start = (n - 1) * partSize;
59
+ const len = Math.min(partSize, size - start);
60
+ const buf = Buffer.alloc(len);
61
+ await fh.read(buf, 0, len, start);
62
+ let lastMsg = '';
63
+ for (let attempt = 0; attempt <= PART_RETRIES; attempt += 1) {
64
+ try {
65
+ const resp = await putFetch(urls[String(n)], { method: 'PUT', body: buf });
66
+ if (resp.ok) {
67
+ const etag = resp.headers && resp.headers.get ? resp.headers.get('etag') : null;
68
+ if (etag) etags[String(n)] = etag.replace(/"/g, ''); // complete wants the bare value
69
+ done += 1;
70
+ progress(`上传 ${filename}:${done}/${count} 片`);
71
+ return;
72
+ }
73
+ lastMsg = `存储返回 ${resp.status}`;
74
+ if (resp.status === 403) await sign([n]); // the part address went stale: ask for a fresh one
75
+ } catch (netErr) {
76
+ lastMsg = `网络错误:${netErr && netErr.message ? netErr.message : netErr}`;
77
+ }
78
+ if (attempt < PART_RETRIES) await sleep(Math.min(8000, 1000 * 2 ** attempt));
79
+ }
80
+ throw makeError(`上传 ${filename} 的第 ${n} 片失败(${lastMsg})。可重新运行同一条命令。`);
81
+ };
82
+
83
+ // worker pool: each worker takes the next unsent part
84
+ const queue = numbers.slice();
85
+ let failure = null;
86
+ const worker = async () => {
87
+ while (!failure) {
88
+ const n = queue.shift();
89
+ if (n === undefined) return;
90
+ try { await sendPart(n); } catch (e) { failure = e; return; }
91
+ }
92
+ };
93
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, count) }, worker));
94
+ if (failure) throw failure;
95
+
96
+ // an ETag the storage did not let us read is taken from the server's list of stored parts
97
+ if (numbers.some((n) => !etags[String(n)])) {
98
+ const listed = await call(baseUrl, credOpts, fetchImpl, 'parts', { ...base, upload_id: uploadId });
99
+ for (const part of listed.parts || []) {
100
+ if (!etags[String(part.part_number)]) etags[String(part.part_number)] = part.etag;
101
+ }
102
+ }
103
+ const parts = numbers.map((n) => ({ part_number: n, etag: etags[String(n)] }));
104
+ if (parts.some((x) => !x.etag)) throw makeError(`上传 ${filename} 没有传完整,请重新运行同一条命令。`);
105
+ try {
106
+ await call(baseUrl, credOpts, fetchImpl, 'complete', { ...base, upload_id: uploadId, parts });
107
+ } catch (e) {
108
+ throw makeError(`合并 ${filename} 失败:${e && e.message ? e.message : e}。可重新运行同一条命令。`);
109
+ }
110
+ } catch (e) {
111
+ try { await call(baseUrl, credOpts, fetchImpl, 'abort', { ...base, upload_id: uploadId }); } catch { /* the platform clears leftovers by itself */ }
112
+ throw e;
113
+ } finally {
114
+ await fh.close();
115
+ }
116
+ }
@@ -158,7 +158,7 @@ export function describeNegotiation(ctx, view, taskId) {
158
158
  } else {
159
159
  const c = n.closed || {};
160
160
  lines.push(` 状态:已结束(${KIND_WORDS[c.kind] || n.status})`);
161
- if (c.refund_pct != null) lines.push(` 退还比例:${c.refund_pct}%${n.amounts ? `(退还 ${money(view, n.amounts.refund_cents)},结算给接单方 ${money(view, n.amounts.release_cents)}${n.amounts.release_net_cents != null ? `,扣掉平台费用和同款分成后接单方预计到手 ${money(view, n.amounts.release_net_cents)}` : ''})` : ''}`);
161
+ if (c.refund_pct != null) lines.push(` 退还比例:${c.refund_pct}%${n.amounts ? `(退还 ${money(view, n.amounts.refund_cents)},结算给接单方 ${money(view, n.amounts.release_cents)}${n.amounts.release_net_cents != null ? `,扣掉平台服务费和同款分成后接单方预计到手 ${money(view, n.amounts.release_net_cents)}` : ''})` : ''}`);
162
162
  if (c.kind === 'court') lines.push(' 已转入 AI 法庭裁决,请用 anytowork libra status 看进展。');
163
163
  }
164
164
  if ((n.entries || []).length) lines.push(' 记录:', ...n.entries.map((en) => entryLine(view, en)));