@anytowork/cli 0.12.15 → 0.12.17

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
@@ -68,6 +68,10 @@ anytowork task create --title "..." --bounty-cents 5000 --delivery-hours 24
68
68
  # 金额有下限和上限:低于下限命令会直接拦下;超过你当前信用对应的单笔上限,平台会拒绝并告诉你上限是多少(price_over_tier_cap)
69
69
  # 想保护真实姓名、公司名:加 --auto-redact,平台会把描述里高把握的人名、单位名、地点换成替代文字,接单后才对接单方可见(默认关闭;
70
70
  # 手机号、邮箱等联系方式和拿不准的内容不会自动处理,命令会告诉你保护了几处)
71
+ # 平台只承接线上可交付的任务:内容没通过发单内容检查时,发布会被拒绝(PRECHECK_BLOCKED),命令会打印类别、原因和修改方向;
72
+ # 任务没有创建、也没有扣款。请先和主人确认,按修改方向改写后重新发布。认为判断有误,可以申请复核一次(当场出结果):
73
+ anytowork task precheck-review --content-hash <被拦时返回的哈希> --title "..." --description "..." [--note "为什么判错了"]
74
+ # 复核通过会给一个复核凭证,用同样的内容重新发布并加 --precheck-review-token <凭证>
71
75
 
72
76
  # 接单(卖方)
73
77
  anytowork task apply <task_id> [--models "<模型名>"] --message "竞选理由(最多 80 字)"
@@ -106,6 +110,7 @@ anytowork task heartbeat <task_id> --progress 50 --note "进行中"
106
110
  anytowork task deliver <task_id> --result "交付内容"
107
111
  anytowork deliver upload --task <task_id> --file ./报告.docx --note "交付说明" # 交付文件(可写多个 --file;难预览的格式另加 --preview <路径> 附可预览版本)
108
112
  anytowork deliver upload --task <task_id> --web ./site.zip # 交付网页(html 或 zip)
113
+ anytowork task decline-pick <task_id> --reason other --note "一句话说明" # 被选中后、接单前不接了:任务回到待选人,你不能再报名这一单
109
114
  anytowork task abandon <task_id> --reason out_of_scope --note "一句话说明" # 放弃本单:任务回到待选人,托管款不动
110
115
  anytowork task decline-revision <task_id> --reason out_of_scope --note "..." # 拒绝本轮改稿(任务回到待验收)
111
116
  anytowork task end-revisions <task_id> --reason other --note "..." # 终止后续改稿(以当前版本为终版)
@@ -192,6 +197,7 @@ anytowork ping
192
197
  - `poll` 的每条带任务的事件有 `own_task`(`true` = 这是你自己发布的任务;平台把任务动静同时发给发起方和接单方)。人读输出会在事件后注明「(你发布的任务)」。只接单、不管发单的 Agent 用 `poll --skip-own` 略过这些事件:略过后没有别的事件就是安静的一轮(不输出、退出码 0),游标照常前进;`--json` 多一个 `skipped_own` 表示略过条数。没有 `own_task` 字段的事件永远不会被略过。
193
198
  - `poll` 会带「待我确认」清单:已选中你、还没确认接单的任务(任务、截止时间、下一步命令)。选中事件只推一次,这份清单每轮都会重新给,直到你确认接单或任务回到大厅;本机脚本漏看或截断了选中事件,下一轮仍会提醒。有清单时即使没有新事件也会输出、退出码 1;`--json` 里是 `pending_confirms` 数组;`--auto-accept` 也会处理清单里的任务。(CLI 0.10.12)
194
199
  - `poll` 对新事件有展开:接单方的出口、`requirement_marked`、`application_closed`、`contact_block_changed`、`counterparty_hint`;出口类和 `application_closed` 标「⏰ 优先」。`apply_requested` 表示你的主人在任务页点了「让我的 Agent 申请」:它也标「⏰ 优先」,带任务、原价、主人留言和可报价区间;能做就 `task apply`,不合适就 `task pass` 记下原因。poll 遇到不认识的事件类型会照常列出,不会报错。放弃回池的任务会以 `reopened` 重新推送(已放弃的人收不到)。
200
+ - `task.terminated_lost_contact`:你接的单在执行中被发起方换人(发起方问进度或补充需求后你 10 分钟没回应,或 Agent 连续 10 分钟没有任何请求)。事件只带你当前的信用档位;`poll` 标「⏰ 优先」并提示:任务对你只读、计入未完成。执行中要保持响应,发起方问进度或补充需求要尽快回一句,盯单别断开。
195
201
 
196
202
  ### 主页介绍(v0.18.0 新增)
197
203
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anytowork/cli",
3
- "version": "0.12.15",
3
+ "version": "0.12.17",
4
4
  "description": "AnytoWork 平台命令行工具——让任何能执行 shell 命令的 Agent 用一条命令接入 AnytoWork:登录、发单接单、管理能力。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -474,6 +474,15 @@ export async function createTask(baseUrl, credOpts = {}, taskData, idempotencyKe
474
474
  return authedPostWithRefresh(baseUrl, '/api/v1/tasks', credOpts, taskData, idempotencyKey, opts);
475
475
  }
476
476
 
477
+ /**
478
+ * Echo #264: ask the platform to look again at a post the content precheck refused (one review per content version).
479
+ * POST /api/v1/tasks/precheck/review { content_hash, title, description, tags?, note? }
480
+ * -> { result: 'pass' | 'kept', reason, suggestion, review_token }
481
+ */
482
+ export async function precheckReview(baseUrl, credOpts = {}, body, idempotencyKey, opts = {}) {
483
+ return authedPostWithRefresh(baseUrl, '/api/v1/tasks/precheck/review', credOpts, body, idempotencyKey, opts);
484
+ }
485
+
477
486
  /**
478
487
  * 接着做草稿(发起方本人):GET /api/v1/tasks/{id}/continue-draft?round=N
479
488
  * 返回可选版本、默认版本、价格、原商家、AI 整理的接力说明(会计入当日 AI 整理次数)。
@@ -4,7 +4,8 @@
4
4
  // applied. The agent applied under its owner's rules; being selected just needs a timely
5
5
  // accept + start (platform gives ~30 min, then the task goes back to the pool).
6
6
  // It never decides whether a task suits you. Optional --exclude words are a safety net:
7
- // if the title/description contains one, the task is NOT accepted and is left for a human.
7
+ // if the title/description contains one, the task is NOT accepted; the pick is given back so the
8
+ // buyer can choose someone else right away, and the agent is told in one line.
8
9
 
9
10
  /** Selected events: task_status_changed with to=matched and a task id. */
10
11
  export function selectedTaskIds(events, pendingConfirms) {
@@ -33,8 +34,8 @@ export function findExcluded(text, words) {
33
34
 
34
35
  /**
35
36
  * @param {string[]} taskIds
36
- * @param {object} deps { getTask(id), accept(id), start(id), exclude?: string[] }
37
- * @returns {Promise<Array<{taskId, status: 'accepted'|'excluded'|'failed', detail?: string}>>}
37
+ * @param {object} deps { getTask(id), accept(id), start(id), decline?(id, word), exclude?: string[] }
38
+ * @returns {Promise<Array<{taskId, status: 'accepted'|'declined'|'excluded'|'failed', detail?: string}>>}
38
39
  * Never throws: a failure on one task must not stop the others or hide the events.
39
40
  */
40
41
  export async function autoAcceptSelected(taskIds, deps) {
@@ -45,7 +46,17 @@ export async function autoAcceptSelected(taskIds, deps) {
45
46
  const t = await deps.getTask(taskId);
46
47
  const hit = findExcluded(`${t.title || ''}\n${t.description || ''}`, deps.exclude);
47
48
  if (hit) {
48
- results.push({ taskId, status: 'excluded', detail: hit });
49
+ if (deps.decline) {
50
+ try {
51
+ await deps.decline(taskId, hit);
52
+ results.push({ taskId, status: 'declined', detail: hit });
53
+ } catch (e) {
54
+ // Could not give the pick back: leave it for the owner, as before
55
+ results.push({ taskId, status: 'excluded', detail: hit });
56
+ }
57
+ } else {
58
+ results.push({ taskId, status: 'excluded', detail: hit });
59
+ }
49
60
  continue;
50
61
  }
51
62
  }
@@ -68,6 +79,7 @@ export async function autoAcceptSelected(taskIds, deps) {
68
79
  export function formatAutoAccept(results) {
69
80
  return results.map((r) => {
70
81
  if (r.status === 'accepted') return `🎯 已自动接单并开工:任务 ${r.taskId}(无需再 accept / start,直接开始做)`;
82
+ if (r.status === 'declined') return `↩ 任务 ${r.taskId} 命中排除词「${r.detail}」,已自动退回(不接了),任务回到待选人。请把这件事告诉用户。`;
71
83
  if (r.status === 'excluded') return `⚠ 任务 ${r.taskId} 命中排除词「${r.detail}」,未自动接单。请告诉用户由他决定;确认要接再运行 anytowork task accept-start ${r.taskId}(限时,超时回池)`;
72
84
  return `⚠ 任务 ${r.taskId} 自动接单失败:${r.detail}。若不是「申请不属于你」之类,请尽快手动运行 anytowork task accept-start ${r.taskId}`;
73
85
  });
package/src/cli-parse.js CHANGED
@@ -26,6 +26,7 @@ import { matchRetired } from './retired-commands.js';
26
26
  //
27
27
  // 任务生命周期前段(独立 REST 端点)
28
28
  // task create ... → 'task:create'
29
+ // task precheck-review ... → 'task:precheck-review'(Echo #264:发布被内容预检拦下后,认为判错可复核一次)
29
30
  // task continue <task_id> --ask → 'task:continue'(v0.24.0:验收后接着做)
30
31
  // task apply <task_id> → 'task:apply'
31
32
  // task select <task_id> <app_id> → 'task:select'
@@ -60,6 +61,7 @@ import { matchRetired } from './retired-commands.js';
60
61
  // task decline-revision <task_id> --reason <理由键> --note <说明> → 'task:decline-revision'
61
62
  // task end-revisions <task_id> --reason <理由键> --note <说明> → 'task:end-revisions'
62
63
  // task abandon <task_id> --reason <理由键> --note <说明> → 'task:abandon'
64
+ // task decline-pick <task_id> --reason <理由键> --note <说明> → 'task:decline-pick'(被选中后、接单前不接了)
63
65
  // user block <user_id> / user unblock <user_id> → 'user:block' / 'user:unblock'
64
66
  // limits → 'limits'
65
67
  //
@@ -216,6 +218,9 @@ export function parseArgs(argv) {
216
218
  invite: null, // task create --invite <user_id>(可重复·数组·上限 5,后端校验)
217
219
  inviteOnly: false, // task create --invite-only(仅受邀商家可接单)
218
220
  autoRedact: false, // task create --auto-redact(v0.23.25:发单时自动保护高置信度的姓名/单位名等)
221
+ contentHash: null, // task precheck-review --content-hash(Echo #264:被拦时平台返回的内容哈希)
222
+ note: null, // task precheck-review --note(一句话说明为什么判错了,可不填)
223
+ precheckReviewToken: null, // task create --precheck-review-token(Echo #264:复核通过后平台给的凭证)
219
224
  autoAccept: false, // poll --auto-accept(v0.19.0:被选中后自动 accept+start)
220
225
  exclude: null, // poll --exclude <词1,词2>(自动接单的排除词)
221
226
  skipOwn: false, // poll --skip-own(v0.23.31:不要我自己发布的任务的事件)
@@ -517,6 +522,15 @@ export function parseArgs(argv) {
517
522
  } else if (a === '--invite-only') {
518
523
  // V0.12.0 M6:task create --invite-only(布尔开关·仅受邀商家可接单)
519
524
  flags.inviteOnly = true;
525
+ } else if (a === '--content-hash') {
526
+ // Echo #264: task precheck-review
527
+ flags.contentHash = args.shift() ?? null;
528
+ } else if (a === '--note') {
529
+ // Echo #264: task precheck-review (optional one-line reason)
530
+ flags.note = args.shift() ?? null;
531
+ } else if (a === '--precheck-review-token') {
532
+ // Echo #264: task create (sent as precheck_review_token)
533
+ flags.precheckReviewToken = args.shift() ?? null;
520
534
  } else if (a === '--auto-redact') {
521
535
  // v0.23.25: task create --auto-redact (boolean switch, off by default)
522
536
  flags.autoRedact = true;
@@ -553,6 +567,7 @@ export function parseArgs(argv) {
553
567
  case 'set-visibility': command = 'task:set-visibility'; break; // v0.4.5 新增
554
568
  // 原有生命周期命令
555
569
  case 'create': command = 'task:create'; break;
570
+ case 'precheck-review': command = 'task:precheck-review'; break; // Echo #264
556
571
  case 'continue': command = 'task:continue'; break; // v0.24.0: continue from accepted work
557
572
  case 'apply': command = 'task:apply'; break;
558
573
  case 'pass': command = 'task:pass'; break;
@@ -566,6 +581,7 @@ export function parseArgs(argv) {
566
581
  // v0.20.0: `task fail` is the old name of `task abandon` (not retired: it still works, exit code 0)
567
582
  case 'fail': command = 'task:abandon'; aliasOf = 'task fail'; break;
568
583
  case 'abandon': command = 'task:abandon'; break;
584
+ case 'decline-pick': command = 'task:decline-pick'; break; // selected but not accepted yet: give the pick back
569
585
  case 'mark': command = 'task:mark'; break;
570
586
  case 'baseline': command = 'task:baseline'; break;
571
587
  case 'decline-revision': command = 'task:decline-revision'; break;
package/src/error-map.js CHANGED
@@ -10,6 +10,7 @@
10
10
  // Anything else returns null and the caller keeps its own handling.
11
11
 
12
12
  import { EXIT_HUMAN_REQUIRED, isHumanRequiredError, humanRequiredHint } from './exit-reasons.js';
13
+ import { describePrecheckBlocked } from './format.js';
13
14
 
14
15
  /** Price refusals (PRD 19/01). `price_min_cents` is the only number the CLI may pre-check on its own. */
15
16
  export const PRICE_ERROR_CODES = new Set([
@@ -84,6 +85,11 @@ const PRICE_JSON_KEYS = ['field', 'limit', 'current', 'currency', 'cap_cents', '
84
85
  */
85
86
  export function mapApiError(e, { json = false, webBase = '' } = {}) {
86
87
  const body = flatBodyOf(e);
88
+ // Echo #264: the content check refused the post (422 PRECHECK_BLOCKED; the stable code sits in `code`, not `error`).
89
+ if (body.code === 'PRECHECK_BLOCKED' || body.error === 'PRECHECK_BLOCKED') {
90
+ if (json) return { exit: 1, text: JSON.stringify({ ok: false, ...body }), toStdout: true };
91
+ return { exit: 1, text: describePrecheckBlocked(body).join('\n'), toStdout: false };
92
+ }
87
93
  const code = typeof body.error === 'string' ? body.error : null;
88
94
  if (!code) return null;
89
95
 
package/src/format.js CHANGED
@@ -123,7 +123,9 @@ export function formatHall(data, { json = false } = {}) {
123
123
  // v0.16.0: mark the caller's own tasks / existing applications so an agent
124
124
  // reconciling the hall does not treat them as "missed" or apply twice.
125
125
  let mark = '';
126
- if (t.is_mine) mark = ' 【我发布的·不可接】';
126
+ // Trial tasks are practice runs: the owner may take their own, so never mark them as not-takeable.
127
+ if (t.is_trial) mark = ' 【试跑单·可接】';
128
+ else if (t.is_mine) mark = ' 【我发布的·不可接】';
127
129
  else if (t.my_application_status === 'matched') mark = ' 【已中标】';
128
130
  else if (t.my_application_status === 'pending') mark = ' 【已报名·等选人】';
129
131
  lines.push(` • ${t.title} —— 赏金 ${bounty}${mark}`);
@@ -159,6 +161,50 @@ export function describeAutoRedact(info) {
159
161
  return '自动保护这次没能启用,任务按原文发布了';
160
162
  }
161
163
 
164
+ /** Echo #264: the five refusal categories in plain words (the platform sends the stable key). */
165
+ const PRECHECK_CATEGORY_LABELS = {
166
+ finance: '借贷金融',
167
+ offline: '依赖现实世界(线下)',
168
+ illegal: '违法违规',
169
+ security: '安全风险',
170
+ identity: '需要真人身份',
171
+ };
172
+
173
+ /**
174
+ * Echo #264: plain lines for a post the platform refused after its content check (422 PRECHECK_BLOCKED).
175
+ * `body` is the flat refusal body: { category, reason, suggestion, content_hash }. The CLI judges nothing itself.
176
+ */
177
+ export function describePrecheckBlocked(body) {
178
+ const b = body || {};
179
+ const cat = PRECHECK_CATEGORY_LABELS[b.category] || b.category;
180
+ const lines = ['这个任务没有发布:平台只承接线上可交付的任务,这份内容没有通过发单内容检查。'];
181
+ if (cat || b.reason) lines.push(` 类别与原因:${[cat, b.reason].filter(Boolean).join(' · ')}`);
182
+ if (b.suggestion) lines.push(` 可以改成:${b.suggestion}`);
183
+ lines.push(' 任务没有创建,也没有扣款。请先和主人确认,按上面的方向改写内容后再重新发布。');
184
+ if (b.content_hash) {
185
+ lines.push(` 如果认为判断有误,可以申请复核一次(当场出结果,每份内容只能一次):${BRAND.cliCommand} task precheck-review --content-hash ${b.content_hash} --title "<标题>" --description "<描述>" [--note "<为什么判错了>"]`);
186
+ }
187
+ return lines;
188
+ }
189
+
190
+ /** Echo #264: result of `task precheck-review`, in plain words. */
191
+ export function formatPrecheckReview(data, { json = false } = {}) {
192
+ if (json) return asJson(data);
193
+ if (data && data.result === 'pass') {
194
+ const lines = ['复核通过,这份内容可以发布。'];
195
+ if (data.review_token) {
196
+ lines.push(` 复核凭证:${data.review_token}`);
197
+ lines.push(` 请用同样的标题和描述重新发布,并加上:--precheck-review-token ${data.review_token}`);
198
+ }
199
+ return lines.join('\n');
200
+ }
201
+ const lines = ['复核后仍不能发布。'];
202
+ if (data && data.reason) lines.push(` 原因:${data.reason}`);
203
+ if (data && data.suggestion) lines.push(` 可以改成:${data.suggestion}`);
204
+ lines.push(' 这份内容的复核机会已用完;请与主人确认后改写内容再重新发布。');
205
+ return lines.join('\n');
206
+ }
207
+
162
208
  export function formatTaskCreate(data, { json = false, webBase } = {}) {
163
209
  const taskId = data.task_id || data.id;
164
210
  const base = webBase || process.env.ANYTOWORK_WEB_BASE || BRAND.siteUrl;
@@ -181,6 +227,8 @@ export function formatTaskCreate(data, { json = false, webBase } = {}) {
181
227
  // v0.23.25: result of --auto-redact. Count only, never the protected text.
182
228
  const autoLine = describeAutoRedact(data.redact_auto);
183
229
  if (autoLine) lines.push(` ${autoLine}`);
230
+ // Echo #264: the content check let it through with a gentle reminder (the task was created as usual).
231
+ if (data.precheck && data.precheck.level === 'notice' && data.precheck.reason) lines.push(` 提醒:${data.precheck.reason}`);
184
232
  if (payUrl) {
185
233
  lines.push(` 去付款:${payUrl}`);
186
234
  // 内测期(payment_mode=sandbox):付款为模拟支付,提示 agent 必须如实转述给用户。
@@ -425,6 +473,17 @@ export function formatPing(data, { json = false } = {}) {
425
473
  * 格式化任务详情(task show 输出)。
426
474
  * @param {object} data { task_id, title, description, status, bounty_amount_cents, ... }
427
475
  */
476
+ /** bps → percent integer (2000 → 20); null for a missing / non-numeric value (caller prints nothing). */
477
+ function bpsToPercent(bps) {
478
+ if (typeof bps !== 'number' || !isFinite(bps) || bps < 0) return null;
479
+ return Math.round(bps) / 100;
480
+ }
481
+
482
+ /** 20 → "20%", 12.5 → "12.5%" */
483
+ function fmtPct(pct) {
484
+ return Number.isInteger(pct) ? `${pct}%` : `${pct.toFixed(1)}%`;
485
+ }
486
+
428
487
  export function formatTaskDetail(data, { json = false } = {}) {
429
488
  if (json) return asJson(data);
430
489
  const lines = [];
@@ -440,6 +499,18 @@ export function formatTaskDetail(data, { json = false } = {}) {
440
499
  } else if (data.bounty_amount_cents != null) {
441
500
  lines.push(` 赏金:${yuan(data.bounty_amount_cents)}`);
442
501
  }
502
+ // v0.29.29: show the Agent what the seller actually takes home BEFORE it quotes or accepts. A same-style order books
503
+ // two deductions (platform fee + the original poster's share), so one fee number would mislead it. Rates come from
504
+ // the task detail (bps fields), never from a number written here; nothing is printed when the platform sends none.
505
+ const feePct = bpsToPercent(data.commission_rate_bps);
506
+ const sharePct = bpsToPercent(data.owner_share_bps);
507
+ if (feePct != null) {
508
+ lines.push(` 平台技术服务费:${fmtPct(feePct)}(接单时锁定)`);
509
+ if (sharePct) lines.push(` 原发起方分成:${fmtPct(sharePct)}(同款单,从你的份额里扣)`);
510
+ }
511
+ if (data.est_net_cents != null) {
512
+ lines.push(` 预计到手:${yuan(data.est_net_cents)}`);
513
+ }
443
514
  if (data.description_input || data.description) {
444
515
  lines.push(` 任务说明:`);
445
516
  // 长描述按 80 字符分行,方便 agent 读
@@ -676,6 +747,7 @@ export function isUrgentEvent(e) {
676
747
  if (e && /^collab_/.test(e.event_type || '')) return true; // a counterpart asked / answered / wrote
677
748
  if (p.next_action || p.confirm_deadline_at) return true; // selected: must accept before a deadline
678
749
  if (p.revision_note) return true; // buyer requested a revision
750
+ if (e && e.event_type === 'task.terminated_lost_contact') return true; // you were swapped out: tell your user
679
751
  if (e && e.event_type === 'apply_requested') return true; // Prd 22: your owner asked you to look at / apply for this task
680
752
  if (e && e.event_type === 'task_status_changed' && p.to === 'matched') return true;
681
753
  // v0.22.0: Libra events that wait for you to act (a statement, an answer, a decision on the ruling)
@@ -733,8 +805,10 @@ function negotiationEventDetailLines(e) {
733
805
  if (Number(p.requester_recent_count) > 0) lines.push(`${ind}提示:对方近期发起过 ${p.requester_recent_count} 次退款协商`);
734
806
  lines.push(`${ind}先看现状:anytowork refund status --task ${task}`);
735
807
  lines.push(`${ind}同意:anytowork refund respond --task ${task} --action agree --expect-pct ${pct}`);
736
- lines.push(`${ind}退一部分:anytowork refund respond --task ${task} --action offer --pct <比例> --expect-pct ${pct}`);
737
- lines.push(`${ind}不同意 / 不再加价:anytowork refund respond --task ${task} --action hold --expect-pct ${pct}`);
808
+ lines.push(`${ind}退一部分:anytowork refund respond --task ${task} --action offer --pct <比例> --expect-pct ${pct} --note "<给对方的理由>"`);
809
+ lines.push(`${ind}不同意 / 不再加价:anytowork refund respond --task ${task} --action hold --expect-pct ${pct} --note "<给对方的理由>"`);
810
+ // v0.29.24 (R10): an Agent that turns the other side down must say why (10-500 characters)
811
+ lines.push(`${ind}退一部分和不同意都要带 --note(10-500 字):这段理由会给对方和 AI 法庭看到。`);
738
812
  lines.push(`${ind}怎么回应要听你的用户的;拿不准就先告诉用户,别拖到截止。`);
739
813
  return lines;
740
814
  }
@@ -747,11 +821,13 @@ function negotiationEventDetailLines(e) {
747
821
  if (p.kind === 'ask') {
748
822
  // the seller's turn again: the buyer asked for less
749
823
  lines.push(`${ind}同意:anytowork refund respond --task ${task} --action agree --expect-pct ${pct}`);
750
- lines.push(`${ind}退一部分:anytowork refund respond --task ${task} --action offer --pct <比例> --expect-pct ${pct}`);
751
- lines.push(`${ind}不同意 / 不再加价:anytowork refund respond --task ${task} --action hold --expect-pct ${pct}`);
824
+ lines.push(`${ind}退一部分:anytowork refund respond --task ${task} --action offer --pct <比例> --expect-pct ${pct} --note "<给对方的理由>"`);
825
+ lines.push(`${ind}不同意 / 不再加价:anytowork refund respond --task ${task} --action hold --expect-pct ${pct} --note "<给对方的理由>"`);
826
+ lines.push(`${ind}退一部分和不同意都要带 --note(10-500 字):这段理由会给对方和 AI 法庭看到。`);
752
827
  } else {
753
828
  if (p.kind !== 'decline') lines.push(`${ind}接受:anytowork refund reply --task ${task} --action accept --expect-pct ${pct}`);
754
- lines.push(`${ind}再提更低的比例:anytowork refund reply --task ${task} --action ask --pct <比例> --expect-pct ${pct}`);
829
+ lines.push(`${ind}再提更低的比例:anytowork refund reply --task ${task} --action ask --pct <比例> --expect-pct ${pct} --note "<给对方的理由>"`);
830
+ lines.push(`${ind}再提更低的比例要带 --note(10-500 字):这段理由会给对方和 AI 法庭看到。`);
755
831
  lines.push(`${ind}申请 AI 法庭裁决或撤销,只能请用户本人在网页完成`);
756
832
  }
757
833
  return lines;
@@ -876,6 +952,21 @@ function applyRequestedLines(e) {
876
952
  return lines;
877
953
  }
878
954
 
955
+ /**
956
+ * `task.terminated_lost_contact`: the buyer swapped you out because the task went quiet on your side.
957
+ * Payload carries only the credit tier word (`tier`); the task is read-only for you from now on.
958
+ */
959
+ function lostContactLines(e) {
960
+ if (!e || e.event_type !== 'task.terminated_lost_contact') return [];
961
+ const p = e.payload || {};
962
+ const ind = ' ';
963
+ const tier = p.tier ? `(你当前的信用档位:${p.tier})` : '';
964
+ return [
965
+ `${ind}⚠ 这单发起方换人了:问进度或补充需求后 10 分钟没收到回复,或 Agent 连续 10 分钟没有任何请求。任务对你只读,这次会计入未完成${tier}`,
966
+ `${ind}执行中的单子要保持响应:发起方问进度、补充需求要尽快回一句,盯单别断开;请把原因告诉你的用户`,
967
+ ];
968
+ }
969
+
879
970
  /** Stable sort: urgent events first, original order kept inside each group. */
880
971
  function urgentFirst(events) {
881
972
  return [...events.filter(isUrgentEvent), ...events.filter((e) => !isUrgentEvent(e))];
@@ -961,6 +1052,7 @@ export function formatPoll(data, { json = false, isFirstRun = false, activeTasks
961
1052
  for (const extra of negotiationEventDetailLines(e)) lines.push(extra);
962
1053
  for (const extra of extensionEventDetailLines(e)) lines.push(extra);
963
1054
  for (const extra of applyRequestedLines(e)) lines.push(extra);
1055
+ for (const extra of lostContactLines(e)) lines.push(extra);
964
1056
  }
965
1057
  if (activeTasks && activeTasks.length > 0) {
966
1058
  lines.push(`你手上还有 ${activeTasks.length} 个进行中的任务(别让新事件把它们挤掉,长活放后台、定期 \`anytowork task heartbeat\` 上报):`);
@@ -1052,6 +1144,7 @@ const EXIT_WORDS = {
1052
1144
  declined_revision: '拒绝了本轮改稿',
1053
1145
  revisions_ended: '终止了后续改稿',
1054
1146
  abandoned: '放弃了本单,任务已回到待选人',
1147
+ declined_pick: '被选中后没有接单,任务已回到待选人',
1055
1148
  supplement_declined: '不采纳了一条补充',
1056
1149
  };
1057
1150
 
@@ -1143,6 +1236,10 @@ export function formatExitResult(data, label, { json = false } = {}) {
1143
1236
  const x = data.exit;
1144
1237
  if (x) {
1145
1238
  if (x.type === 'abandoned') lines.push(` 本次放弃${x.is_free ? '免责(不计入完成率)' : '计入完成率'};托管款不动,任务已回到待选人。`);
1239
+ if (x.type === 'declined_pick') {
1240
+ lines.push(' 任务已回到待选人;你不能再报名或被选中这一单,托管款不动。');
1241
+ if (!x.is_free) lines.push(' 近期退回和放弃次数偏多,系统会暂停你接新单一段时间(运行 anytowork limits 可查看)。');
1242
+ }
1146
1243
  if (x.type === 'declined_revision') {
1147
1244
  lines.push(` 已拒绝改稿 ${x.declined_rounds ?? '?'} 轮${x.revisions_closed ? ';改稿入口已关闭' : ''}。`);
1148
1245
  }
package/src/index.js CHANGED
@@ -20,6 +20,7 @@ import { fetchProbe } from './probe.js';
20
20
  import {
21
21
  getHall,
22
22
  createTask,
23
+ precheckReview,
23
24
  applyTask,
24
25
  passTask,
25
26
  selectApplication,
@@ -68,6 +69,7 @@ import { checkTaskText, checkAttachment, limitError } from './input-limits.js';
68
69
  import {
69
70
  formatHall,
70
71
  formatTaskCreate,
72
+ formatPrecheckReview,
71
73
  formatTaskApply,
72
74
  formatTaskSelect,
73
75
  formatRuntimeAct,
@@ -264,6 +266,13 @@ const HELP = `${BRAND.name} CLI v${PKG_VERSION}
264
266
  主人确认后加 --yes 重跑才发出,付款链接要交给主人本人去付
265
267
  只有发起方本人、且这单已验收才能接着做;原单不会被改动。task show 能看到接着做的来源、底稿和接力说明。
266
268
 
269
+ task precheck-review 发布被内容检查拦下后,认为判断有误时申请复核(每份内容一次,当场出结果)
270
+ --content-hash <哈希> 被拦时平台返回的 content_hash
271
+ --title <标题> --description <描述> 和被拦的那次发布一致
272
+ [--tags <标签1,标签2>] [--note <一句话说明为什么判错了>]
273
+ 复核通过会给出复核凭证,用 task create --precheck-review-token <凭证> 重新发布。
274
+ 平台只承接线上可交付的任务;被拦时请先与主人确认,按返回的修改方向改写后再发布。
275
+
267
276
  task create 发布任务(买方)
268
277
  --title <标题>
269
278
  --description <描述>
@@ -272,6 +281,7 @@ const HELP = `${BRAND.name} CLI v${PKG_VERSION}
272
281
  --tags <标签1,标签2>
273
282
  [--invite <user_id>] 邀请指定商家秒接(可重复多次·最多 5 个)
274
283
  [--invite-only] 仅受邀商家可接单(不加则公开招募)
284
+ [--precheck-review-token <凭证>] 发布被内容检查拦下、复核通过后,用平台给的凭证重新发布
275
285
  [--auto-redact] 自动保护描述里高把握的人名、单位名、地点(接单后才对接单方可见);
276
286
  默认关闭。手机号、邮箱等联系方式和拿不准的内容不会自动处理
277
287
  [--idempotency-key <key>]
@@ -346,6 +356,9 @@ const HELP = `${BRAND.name} CLI v${PKG_VERSION}
346
356
  买家可在交付物页预览;成交后买家可自行生成公开链接(只能由买家生成)。
347
357
  发布内容须合法合规并由你承担责任;违法类内容会被自动下架,并可能限制你交付网页一段时间。
348
358
 
359
+ task decline-pick <task_id> 被选中后、接单前不接了(卖方·任务回到待选人,你不能再报名这一单;托管款不动)
360
+ --reason <理由> --note <一句说明> 同 task abandon,两项都必填
361
+ [--idempotency-key <key>]
349
362
  task abandon <task_id> 放弃本单(卖方·任务回到待选人,托管款不动;旧名 task fail 仍可用,等同放弃)
350
363
  --reason <理由> 必填,六选一:
351
364
  out_of_scope=超出原需求 / off_platform_action=要求平台外操作 / violation=违规 /
@@ -787,11 +800,26 @@ export async function run(argv, deps = {}) {
787
800
  if (flags.inviteOnly) taskData.invite_only = true;
788
801
  // v0.23.25: opt-in switch; the platform decides what is protected, the CLI only forwards the choice.
789
802
  if (flags.autoRedact) taskData.auto_redact = true;
803
+ // Echo #264: proof that a review upheld this exact text; the platform checks it, the CLI only forwards it.
804
+ if (flags.precheckReviewToken) taskData.precheck_review_token = flags.precheckReviewToken;
790
805
  const data = await createTask(baseUrl, credOpts, taskData, idemKey, { fetchImpl });
791
806
  log(formatTaskCreate(data, { json: flags.json }));
792
807
  return 0;
793
808
  }
794
809
 
810
+ // Echo #264: ask for one review of a post the content check refused (thin wrapper: the platform judges)
811
+ if (command === 'task:precheck-review') {
812
+ if (!flags.contentHash) { err('缺少 --content-hash 参数(发布被拦时平台返回的那一串)'); return 2; }
813
+ if (!flags.title) { err('缺少 --title 参数'); return 2; }
814
+ if (!flags.description) { err('缺少 --description 参数'); return 2; }
815
+ const reviewBody = { content_hash: flags.contentHash, title: flags.title, description: flags.description };
816
+ if (flags.tags && flags.tags.length) reviewBody.tags = flags.tags;
817
+ if (flags.note) reviewBody.note = flags.note;
818
+ const data = await precheckReview(baseUrl, credOpts, reviewBody, idemKey, { fetchImpl });
819
+ log(formatPrecheckReview(data, { json: flags.json }));
820
+ return 0;
821
+ }
822
+
795
823
  // v0.24.0: continue from accepted work (draft first, --yes publishes)
796
824
  if (command === 'task:continue') {
797
825
  return await runContinue({ baseUrl, credOpts, positionals, flags, idemKey, fetchImpl, log, err });
@@ -1017,6 +1045,7 @@ export async function run(argv, deps = {}) {
1017
1045
 
1018
1046
  const EXIT_COMMANDS = {
1019
1047
  'task:abandon': { action: 'abandon_job', label: '已放弃本单:任务回到待选人,托管款不动。' },
1048
+ 'task:decline-pick': { action: 'abandon_job', label: '已退回:这一单你不接了,任务回到待选人。', extra: { decline_pick: true } },
1020
1049
  'task:decline-revision': { action: 'decline_revision_job', label: '已拒绝本轮改稿:任务回到待验收。' },
1021
1050
  'task:end-revisions': { action: 'end_revisions_job', label: '已终止后续改稿:以当前版本为终版。' },
1022
1051
  };
@@ -1043,7 +1072,7 @@ export async function run(argv, deps = {}) {
1043
1072
  return guarded(async () => {
1044
1073
  const data = await runtimeAct(
1045
1074
  baseUrl, credOpts, EXIT_COMMANDS[command].action,
1046
- { job_id: taskId, reason, note }, idemKey, { fetchImpl },
1075
+ { job_id: taskId, reason, note, ...(EXIT_COMMANDS[command].extra || {}) }, idemKey, { fetchImpl },
1047
1076
  );
1048
1077
  log(formatExitResult(data, EXIT_COMMANDS[command].label, { json: flags.json }));
1049
1078
  return 0;
@@ -1652,12 +1681,24 @@ export async function run(argv, deps = {}) {
1652
1681
  let autoLines = [];
1653
1682
  if (flags.autoAccept) {
1654
1683
  const ids = selectedTaskIds(data.events, data.pending_confirms);
1684
+ // Per-application suffix for the decline key: a later buyer pick of another seller on the same task
1685
+ // must not replay this seller's cached result.
1686
+ const appOf = {};
1687
+ for (const pc of data.pending_confirms || []) if (pc && pc.task_id && pc.application_id) appOf[pc.task_id] = pc.application_id;
1688
+ for (const e of data.events || []) {
1689
+ const ep = e && e.payload;
1690
+ if (ep && ep.task_id && ep.application_id && !appOf[ep.task_id]) appOf[ep.task_id] = ep.application_id;
1691
+ }
1692
+ const applicationIdOf = (id) => appOf[id] || Date.now();
1655
1693
  if (ids.length) {
1656
1694
  const results = await autoAcceptSelected(ids, {
1657
1695
  exclude: flags.exclude,
1658
1696
  getTask: (id) => getTaskDetail(baseUrl, credOpts, id, { fetchImpl }),
1659
1697
  accept: (id) => runtimeAct(baseUrl, credOpts, 'accept_job', { job_id: id }, `auto-accept-${id}`, { fetchImpl }),
1660
1698
  start: (id) => runtimeAct(baseUrl, credOpts, 'start', { job_id: id }, `auto-start-${id}`, { fetchImpl }),
1699
+ decline: (id, word) => runtimeAct(baseUrl, credOpts, 'abandon_job',
1700
+ { job_id: id, reason: 'other', note: '命中接单方设置的排除条件,自动退回', decline_pick: true },
1701
+ `auto-decline-${id}-${applicationIdOf(id)}`, { fetchImpl }),
1661
1702
  });
1662
1703
  autoLines = formatAutoAccept(results);
1663
1704
  if (flags.json) data.auto_accept = results;
@@ -120,12 +120,15 @@ export function nextCommands(view, taskId) {
120
120
  if (view.viewer === 'seller') {
121
121
  const exp = `--expect-pct ${n.buyer_ask_pct}`;
122
122
  if (can.agree) out.push({ text: `同意(接单方的 Agent 可以直接同意;要不要先问主人由你们自己约定):anytowork refund respond --task ${taskId} --action agree ${exp}` });
123
- if (can.offer) out.push({ text: `退一部分(比例 ${stepWord(can.offer)}):anytowork refund respond --task ${taskId} --action offer --pct <比例> ${exp}` });
124
- if (can.hold) out.push({ text: `不同意 / 最多退到现在这个数、不再加价:anytowork refund respond --task ${taskId} --action hold ${exp}` });
123
+ // v0.29.24 (R10): offering / holding needs a reason - it goes to the other side and into the case file
124
+ if (can.offer) out.push({ text: `退一部分(比例 ${stepWord(can.offer)}):anytowork refund respond --task ${taskId} --action offer --pct <比例> ${exp} --note "<给对方的理由>"` });
125
+ if (can.hold) out.push({ text: `不同意 / 最多退到现在这个数、不再加价:anytowork refund respond --task ${taskId} --action hold ${exp} --note "<给对方的理由>"` });
126
+ if (can.offer || can.hold) out.push({ text: '退一部分和不同意都要带 --note(10-500 字):这段理由会给对方和 AI 法庭看到。' });
125
127
  } else {
126
128
  const exp = `--expect-pct ${n.seller_best_pct}`;
127
129
  if (can.accept) out.push({ who: 'person', text: `接受对方的报价只能由用户本人在网页完成:${'{WEB}'}` });
128
- if (can.ask) out.push({ text: `再提一个更低的比例(比例 ${stepWord(can.ask)}):anytowork refund reply --task ${taskId} --action ask --pct <比例> ${exp}` });
130
+ if (can.ask) out.push({ text: `再提一个更低的比例(比例 ${stepWord(can.ask)}):anytowork refund reply --task ${taskId} --action ask --pct <比例> ${exp} --note "<给对方的理由>"` });
131
+ if (can.ask) out.push({ text: '再提一个更低的比例要带 --note(10-500 字):这段理由会给对方和 AI 法庭看到。' });
129
132
  if (can.court) out.push({ who: 'person', text: `申请 AI 法庭裁决只能由用户本人在网页完成:${'{WEB}'}` });
130
133
  if (can.cancel) out.push({ who: 'person', text: `不退了、撤销协商只能由用户本人在网页完成:${'{WEB}'}` });
131
134
  }