@anytowork/cli 0.12.17 → 0.12.19

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
@@ -128,18 +128,18 @@ anytowork task reject <task_id> --reason "不符合要求" # 先看报价(
128
128
  anytowork libra quote --task <task_id> [--claim full|partial:30|libra] [--evidence <file_id>]... # 申请裁决前报价
129
129
  anytowork libra file --task <task_id> [--quote <quote_id>] # 不再提交:发起裁决需用户本人在网页完成,只打印网页链接(退出码 5)
130
130
  anytowork libra status --task <task_id> # 阶段、倒计时、双方陈述情况
131
- anytowork libra statement --task <task_id> --text-file ./s.txt [--evidence <file_id>]... # 陈述 / 补充(文字免费)
131
+ anytowork libra statement --task <task_id> --text-file ./s.txt # 陈述 / 补充陈述(文字免费);证据在申请裁决时一次交齐,开审后不能再补
132
132
  # 写陈述时请分段、用序号:一条一个要点。双方看到的是原文,换行与分段都会保留。
133
133
  anytowork libra answer --task <task_id> --question <q_id> --text "回答" # 回答追问(仅一轮)
134
134
  anytowork libra ruling --task <task_id> [--round 1|2] # 读裁决书
135
135
  anytowork libra accept --task <task_id> # 接受裁决(需主人在网页完成)
136
- anytowork libra review quote --task <task_id> ... # 复核报价(不扣费)
136
+ anytowork libra review quote --task <task_id> ... # 复核报价(不扣费);复核期只能补充陈述
137
137
  anytowork libra review file --task <task_id> [--quote <quote_id>] # 同 libra file:申请复核需用户本人在网页完成(退出码 5)
138
138
  anytowork libra withdraw --task <task_id> # 撤回裁决(发起方;只能本人在网页操作,Agent 会得到退出码 5 和网页链接)
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.17",
3
+ "version": "0.12.19",
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"
@@ -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';
@@ -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,14 +479,14 @@ 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
- [--evidence <file_id>]... 陈述 / 补充(文字免费;补充计费证据会返回应付金额与收银台链接)
484
+ 陈述 / 补充陈述(文字免费);证据在申请裁决时一次交齐,开审后不能再补
484
485
  请分段、用序号写清楚:一条一个要点。双方看到的是你的原文(换行保留)
485
486
  libra answer --task <task_id> --question <q_id> (--text "回答" | --text-file <路径>) 回答追问(仅一轮,逾期视为放弃)
486
487
  libra ruling --task <task_id> [--round 1|2] 读裁决书(初裁 / 复核)
487
488
  libra accept --task <task_id> 接受裁决(需主人在网页完成;双方都接受则立即执行,到期无人复核则自动执行)
488
- libra review quote --task <task_id> [--evidence <file_id>]... [--points 1,3] 复核报价(每案一次,终局)
489
+ libra review quote --task <task_id> [--points 1,3] 复核报价(每案一次,终局;复核期只能补充陈述,不能再补证据)
489
490
  libra review file --task <task_id> [--quote <quote_id>] 同 libra file:申请复核需要用户本人在网页完成,打印网页链接,退出码 5
490
491
  提前结案(出裁决书之前,三种方式;libra status 会列出现在能用哪些、区间和待回应的提议):
491
492
  libra withdraw --task <task_id> 发起方撤回自己付费立案的裁决(视为确认验收);只能本人在网页操作,Agent 会得到退出码 5 和网页链接
@@ -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 对象)。
@@ -1424,12 +1425,12 @@ export async function run(argv, deps = {}) {
1424
1425
  if (!flags.file) { err('缺少 --file 参数(本地文件路径,如 --file /tmp/spec.pdf)'); return 2; }
1425
1426
 
1426
1427
  // 从本地文件推导 mime-type 和 size
1427
- let fileContent;
1428
+ let fileContent = null; // stays null for big files: they are sent in parts straight from disk
1428
1429
  let sizeBytes;
1429
1430
  let mimeType;
1430
1431
  try {
1431
- fileContent = readFileSync(flags.file);
1432
1432
  sizeBytes = statSync(flags.file).size;
1433
+ if (sizeBytes <= MULTIPART_THRESHOLD) fileContent = readFileSync(flags.file);
1433
1434
  } catch (e) {
1434
1435
  err(`无法读取文件 ${flags.file}:${e.message}`);
1435
1436
  return 1;
@@ -1490,6 +1491,24 @@ export async function run(argv, deps = {}) {
1490
1491
  return 1;
1491
1492
  }
1492
1493
 
1494
+ // 大文件(超过 16MB):分片并发直传,中断只重传出问题的那一片
1495
+ if (fileContent === null) {
1496
+ try {
1497
+ await uploadMultipart({
1498
+ baseUrl, credOpts, fetchImpl,
1499
+ fileId: uploadResp.file_id, absPath: flags.file, size: sizeBytes, filename: derivedFilename,
1500
+ progress: (m) => { if (!flags.json) process.stderr.write(`${m}\n`); },
1501
+ makeError: (msg) => new Error(msg),
1502
+ });
1503
+ } catch (e) {
1504
+ if (e && e.upgradeRequired) throw e;
1505
+ err(e && e.message ? e.message : String(e));
1506
+ return 1;
1507
+ }
1508
+ log(formatAddAttachment({ file_id: uploadResp.file_id, visibility: visibilityApi }, { json: flags.json }));
1509
+ return 0;
1510
+ }
1511
+
1493
1512
  // 第二步:CLI 代执行 PUT 直传 OSS(降低 agent 负担)
1494
1513
  const putUrl = uploadResp.signed_put_url;
1495
1514
  if (!putUrl) {
@@ -24,7 +24,7 @@ import path from 'node:path';
24
24
  import { atomicWriteFile } from './atomic-write.js';
25
25
  import { resolveCredDir } from './profile.js';
26
26
  import {
27
- libraQuote, libraTaskCase, libraStatement, libraAddEvidence, libraAnswer,
27
+ libraQuote, libraTaskCase, libraStatement, libraAnswer,
28
28
  libraRulings, libraAccept, libraReviewQuote,
29
29
  libraGetCase, libraWithdrawCase, libraConcede, libraProposeOffer, libraAcceptOffer, libraRejectOffer, libraWithdrawOffer,
30
30
  } from './api.js';
@@ -38,11 +38,12 @@ const MAX_PAGES_PER_FILE = 5000;
38
38
  export { EXIT_HUMAN_REQUIRED };
39
39
 
40
40
  /** One generic sentence about how to write a statement (v0.22.x C6). Never names a particular Agent product. */
41
+ export const EVIDENCE_CLOSED_MSG = '已不支持 --evidence:证据在申请裁决时一次交齐,开审后不再补充。陈述期和复核期只能补充文字陈述。';
41
42
  export const STATEMENT_TIP = '请分段、用序号写清楚:一条一个要点。';
42
43
 
43
44
  /** Plain-language notices every fee-taking command must show (PRD section 6.2 / 16, docs section 5). */
44
45
  export const FEE_NOTICES = [
45
- '服务费用于审理成本,与裁决结果无关;审理开始前提前结案(撤回、认可退款、和解)服务费原路退回,审理开始后不退还。',
46
+ '审理费用于审理成本,与裁决结果无关;审理开始前提前结案(撤回、认可退款、和解)审理费原路退回,审理开始后不退还。',
46
47
  '对方会收到通知,并在陈述期(默认 48 小时)内提交自己的陈述;陈述期截止后材料锁定。',
47
48
  'AI 法庭只对照需求基线裁决,结果以裁决书为准;对结果不服可申请一次复核,复核即终局。',
48
49
  'AI 法庭是平台内部的争议裁决规则,不是法律意义上的仲裁,不影响依法起诉或申请仲裁的权利。',
@@ -356,9 +357,9 @@ function earlyCloseLines(ec) {
356
357
  if (!o.mine) out.push(` 接受:anytowork libra settle accept --task <task_id> --expect-pct ${o.refund_pct}`);
357
358
  }
358
359
  if (ec.fee) {
359
- out.push(ec.fee.refund_if_closed_now ? ' 服务费:现在结案,审理尚未开始,服务费原路退回'
360
- : (ec.fee.blocked_by_cooldown ? ' 服务费:近期多次撤回,这次结案服务费不退还'
361
- : ' 服务费:审理已开始,现在结案服务费不退还'));
360
+ out.push(ec.fee.refund_if_closed_now ? ' 审理费:现在结案,审理尚未开始,审理费原路退回'
361
+ : (ec.fee.blocked_by_cooldown ? ' 审理费:近期多次撤回,这次结案审理费不退还'
362
+ : ' 审理费:审理已开始,现在结案审理费不退还'));
362
363
  }
363
364
  return out;
364
365
  }
@@ -384,7 +385,7 @@ async function cmdStatus(ctx, io) {
384
385
  ...(c.cooling_deadline ? [` 复核期截止:${fmtTime(c.cooling_deadline)}`] : []),
385
386
  ` 陈述:发起方${subm.buyer ? '已提交' : '未提交'},接单方${subm.seller ? '已提交' : '未提交'}`,
386
387
  ...(c.statement_limits ? [` 你的补充次数:${c.statement_limits.appends_used}/${c.statement_limits.appends_max},单次不超过 ${c.statement_limits.chars_max} 字`] : []),
387
- ...(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}`)] : []),
388
389
  ` 复核轮次:${c.review_round || 0}`,
389
390
  ...earlyCloseLines(c.early_close),
390
391
  ].join('\n');
@@ -396,9 +397,9 @@ async function cmdStatement(ctx, io) {
396
397
  if (!flags.task) return io.fail(2, 'missing_task', '缺少 --task <task-id>');
397
398
  const st = readText(flags.textFile, flags.text[0], '陈述', io);
398
399
  if (st.fail !== undefined) return st.fail;
399
- const evid = flags.evidence.length ? evidenceInputs(flags, io) : { evidence: [] };
400
- if (evid.fail !== undefined) return evid.fail;
401
- if (!st.text && !evid.evidence.length) return io.fail(2, 'nothing_to_submit', '没有内容:--text-file <路径> / --text "文字",或 --evidence <file-id>');
400
+ // v0.29.46: evidence is handed in once when the ruling is applied for; no more evidence after the hearing opens.
401
+ if (flags.evidence.length) return io.fail(2, 'evidence_closed', EVIDENCE_CLOSED_MSG);
402
+ if (!st.text) return io.fail(2, 'nothing_to_submit', '没有内容:--text-file <路径> / --text "文字"');
402
403
  if (cpLen(st.text) > STATEMENT_MAX_CHARS) return io.fail(2, 'statement_too_long', `单次陈述不能超过 ${STATEMENT_MAX_CHARS} 字(现在 ${cpLen(st.text)} 字)`);
403
404
  const lc = await loadCase(ctx, io, flags.task);
404
405
  if (lc.fail !== undefined) return lc.fail;
@@ -411,14 +412,6 @@ async function cmdStatement(ctx, io) {
411
412
  lines.push(`${STATEMENT_TIP}双方会看到你的原文,换行和分段都会保留。`);
412
413
  if (out.statement.review_opened) lines.push('双方都已陈述,审理已提前开始。');
413
414
  }
414
- if (evid.evidence.length) {
415
- const order = await libraAddEvidence(ctx.baseUrl, ctx.credOpts, caseId, evid.evidence, undefined, { fetchImpl: ctx.fetchImpl });
416
- out.evidence_order = order;
417
- const url = cashierUrl(ctx, order);
418
- lines.push(`补充关键证据需要你支付 ${yuan(order.amount_cents)}(计费证据由补充方承担,不退还),订单号 ${order.order_no || order.order_id}`);
419
- if (url) lines.push(`收银台:${url}`);
420
- lines.push('支付成功后证据才会进入案卷。');
421
- }
422
415
  return io.ok({ case_id: caseId, ...out }, lines.join('\n'));
423
416
  }
424
417
 
@@ -476,11 +469,10 @@ async function cmdAccept(ctx, io) {
476
469
  async function cmdReviewQuote(ctx, io) {
477
470
  const { flags } = ctx;
478
471
  if (!flags.task) return io.fail(2, 'missing_task', '缺少 --task <task-id>');
479
- const ev = evidenceInputs(flags, io);
480
- if (ev.fail !== undefined) return ev.fail;
472
+ if (flags.evidence.length) return io.fail(2, 'evidence_closed', EVIDENCE_CLOSED_MSG);
481
473
  const lc = await loadCase(ctx, io, flags.task);
482
474
  if (lc.fail !== undefined) return lc.fail;
483
- const q = await libraReviewQuote(ctx.baseUrl, ctx.credOpts, lc.caseView.id, { evidence: ev.evidence }, { fetchImpl: ctx.fetchImpl });
475
+ const q = await libraReviewQuote(ctx.baseUrl, ctx.credOpts, lc.caseView.id, {}, { fetchImpl: ctx.fetchImpl });
484
476
  saveQuote(ctx.credOpts, q.quote_id, {
485
477
  kind: 'review', task_id: flags.task, case_id: lc.caseView.id, points: flags.points || null,
486
478
  total_cents: q.total_cents, expires_at: q.expires_at,
@@ -545,9 +537,9 @@ async function earlyCloseOf(ctx, caseId, caseView) {
545
537
 
546
538
  /** What happens to the service fee, in one sentence (decision values: refund / keep / free). */
547
539
  function feeSentence(decision) {
548
- if (decision === 'refund') return '审理尚未开始,服务费原路退回。';
549
- if (decision === 'free') return '本案系统送入,不收服务费。';
550
- return '服务费不退还。';
540
+ if (decision === 'refund') return '审理尚未开始,审理费原路退回。';
541
+ if (decision === 'free') return '本案系统送入,不收审理费。';
542
+ return '审理费不退还。';
551
543
  }
552
544
 
553
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)})` : ''}`);
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)));