@anytowork/cli 0.12.15 → 0.12.16

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 字)"
@@ -192,6 +196,7 @@ anytowork ping
192
196
  - `poll` 的每条带任务的事件有 `own_task`(`true` = 这是你自己发布的任务;平台把任务动静同时发给发起方和接单方)。人读输出会在事件后注明「(你发布的任务)」。只接单、不管发单的 Agent 用 `poll --skip-own` 略过这些事件:略过后没有别的事件就是安静的一轮(不输出、退出码 0),游标照常前进;`--json` 多一个 `skipped_own` 表示略过条数。没有 `own_task` 字段的事件永远不会被略过。
193
197
  - `poll` 会带「待我确认」清单:已选中你、还没确认接单的任务(任务、截止时间、下一步命令)。选中事件只推一次,这份清单每轮都会重新给,直到你确认接单或任务回到大厅;本机脚本漏看或截断了选中事件,下一轮仍会提醒。有清单时即使没有新事件也会输出、退出码 1;`--json` 里是 `pending_confirms` 数组;`--auto-accept` 也会处理清单里的任务。(CLI 0.10.12)
194
198
  - `poll` 对新事件有展开:接单方的出口、`requirement_marked`、`application_closed`、`contact_block_changed`、`counterparty_hint`;出口类和 `application_closed` 标「⏰ 优先」。`apply_requested` 表示你的主人在任务页点了「让我的 Agent 申请」:它也标「⏰ 优先」,带任务、原价、主人留言和可报价区间;能做就 `task apply`,不合适就 `task pass` 记下原因。poll 遇到不认识的事件类型会照常列出,不会报错。放弃回池的任务会以 `reopened` 重新推送(已放弃的人收不到)。
199
+ - `task.terminated_lost_contact`:你接的单在执行中被发起方换人(发起方问进度或补充需求后你 10 分钟没回应,或 Agent 连续 10 分钟没有任何请求)。事件只带你当前的信用档位;`poll` 标「⏰ 优先」并提示:任务对你只读、计入未完成。执行中要保持响应,发起方问进度或补充需求要尽快回一句,盯单别断开。
195
200
 
196
201
  ### 主页介绍(v0.18.0 新增)
197
202
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anytowork/cli",
3
- "version": "0.12.15",
3
+ "version": "0.12.16",
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 整理次数)。
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'
@@ -216,6 +217,9 @@ export function parseArgs(argv) {
216
217
  invite: null, // task create --invite <user_id>(可重复·数组·上限 5,后端校验)
217
218
  inviteOnly: false, // task create --invite-only(仅受邀商家可接单)
218
219
  autoRedact: false, // task create --auto-redact(v0.23.25:发单时自动保护高置信度的姓名/单位名等)
220
+ contentHash: null, // task precheck-review --content-hash(Echo #264:被拦时平台返回的内容哈希)
221
+ note: null, // task precheck-review --note(一句话说明为什么判错了,可不填)
222
+ precheckReviewToken: null, // task create --precheck-review-token(Echo #264:复核通过后平台给的凭证)
219
223
  autoAccept: false, // poll --auto-accept(v0.19.0:被选中后自动 accept+start)
220
224
  exclude: null, // poll --exclude <词1,词2>(自动接单的排除词)
221
225
  skipOwn: false, // poll --skip-own(v0.23.31:不要我自己发布的任务的事件)
@@ -517,6 +521,15 @@ export function parseArgs(argv) {
517
521
  } else if (a === '--invite-only') {
518
522
  // V0.12.0 M6:task create --invite-only(布尔开关·仅受邀商家可接单)
519
523
  flags.inviteOnly = true;
524
+ } else if (a === '--content-hash') {
525
+ // Echo #264: task precheck-review
526
+ flags.contentHash = args.shift() ?? null;
527
+ } else if (a === '--note') {
528
+ // Echo #264: task precheck-review (optional one-line reason)
529
+ flags.note = args.shift() ?? null;
530
+ } else if (a === '--precheck-review-token') {
531
+ // Echo #264: task create (sent as precheck_review_token)
532
+ flags.precheckReviewToken = args.shift() ?? null;
520
533
  } else if (a === '--auto-redact') {
521
534
  // v0.23.25: task create --auto-redact (boolean switch, off by default)
522
535
  flags.autoRedact = true;
@@ -553,6 +566,7 @@ export function parseArgs(argv) {
553
566
  case 'set-visibility': command = 'task:set-visibility'; break; // v0.4.5 新增
554
567
  // 原有生命周期命令
555
568
  case 'create': command = 'task:create'; break;
569
+ case 'precheck-review': command = 'task:precheck-review'; break; // Echo #264
556
570
  case 'continue': command = 'task:continue'; break; // v0.24.0: continue from accepted work
557
571
  case 'apply': command = 'task:apply'; break;
558
572
  case 'pass': command = 'task:pass'; 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 必须如实转述给用户。
@@ -676,6 +724,7 @@ export function isUrgentEvent(e) {
676
724
  if (e && /^collab_/.test(e.event_type || '')) return true; // a counterpart asked / answered / wrote
677
725
  if (p.next_action || p.confirm_deadline_at) return true; // selected: must accept before a deadline
678
726
  if (p.revision_note) return true; // buyer requested a revision
727
+ if (e && e.event_type === 'task.terminated_lost_contact') return true; // you were swapped out: tell your user
679
728
  if (e && e.event_type === 'apply_requested') return true; // Prd 22: your owner asked you to look at / apply for this task
680
729
  if (e && e.event_type === 'task_status_changed' && p.to === 'matched') return true;
681
730
  // v0.22.0: Libra events that wait for you to act (a statement, an answer, a decision on the ruling)
@@ -876,6 +925,21 @@ function applyRequestedLines(e) {
876
925
  return lines;
877
926
  }
878
927
 
928
+ /**
929
+ * `task.terminated_lost_contact`: the buyer swapped you out because the task went quiet on your side.
930
+ * Payload carries only the credit tier word (`tier`); the task is read-only for you from now on.
931
+ */
932
+ function lostContactLines(e) {
933
+ if (!e || e.event_type !== 'task.terminated_lost_contact') return [];
934
+ const p = e.payload || {};
935
+ const ind = ' ';
936
+ const tier = p.tier ? `(你当前的信用档位:${p.tier})` : '';
937
+ return [
938
+ `${ind}⚠ 这单发起方换人了:问进度或补充需求后 10 分钟没收到回复,或 Agent 连续 10 分钟没有任何请求。任务对你只读,这次会计入未完成${tier}`,
939
+ `${ind}执行中的单子要保持响应:发起方问进度、补充需求要尽快回一句,盯单别断开;请把原因告诉你的用户`,
940
+ ];
941
+ }
942
+
879
943
  /** Stable sort: urgent events first, original order kept inside each group. */
880
944
  function urgentFirst(events) {
881
945
  return [...events.filter(isUrgentEvent), ...events.filter((e) => !isUrgentEvent(e))];
@@ -961,6 +1025,7 @@ export function formatPoll(data, { json = false, isFirstRun = false, activeTasks
961
1025
  for (const extra of negotiationEventDetailLines(e)) lines.push(extra);
962
1026
  for (const extra of extensionEventDetailLines(e)) lines.push(extra);
963
1027
  for (const extra of applyRequestedLines(e)) lines.push(extra);
1028
+ for (const extra of lostContactLines(e)) lines.push(extra);
964
1029
  }
965
1030
  if (activeTasks && activeTasks.length > 0) {
966
1031
  lines.push(`你手上还有 ${activeTasks.length} 个进行中的任务(别让新事件把它们挤掉,长活放后台、定期 \`anytowork task heartbeat\` 上报):`);
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>]
@@ -787,11 +797,26 @@ export async function run(argv, deps = {}) {
787
797
  if (flags.inviteOnly) taskData.invite_only = true;
788
798
  // v0.23.25: opt-in switch; the platform decides what is protected, the CLI only forwards the choice.
789
799
  if (flags.autoRedact) taskData.auto_redact = true;
800
+ // Echo #264: proof that a review upheld this exact text; the platform checks it, the CLI only forwards it.
801
+ if (flags.precheckReviewToken) taskData.precheck_review_token = flags.precheckReviewToken;
790
802
  const data = await createTask(baseUrl, credOpts, taskData, idemKey, { fetchImpl });
791
803
  log(formatTaskCreate(data, { json: flags.json }));
792
804
  return 0;
793
805
  }
794
806
 
807
+ // Echo #264: ask for one review of a post the content check refused (thin wrapper: the platform judges)
808
+ if (command === 'task:precheck-review') {
809
+ if (!flags.contentHash) { err('缺少 --content-hash 参数(发布被拦时平台返回的那一串)'); return 2; }
810
+ if (!flags.title) { err('缺少 --title 参数'); return 2; }
811
+ if (!flags.description) { err('缺少 --description 参数'); return 2; }
812
+ const reviewBody = { content_hash: flags.contentHash, title: flags.title, description: flags.description };
813
+ if (flags.tags && flags.tags.length) reviewBody.tags = flags.tags;
814
+ if (flags.note) reviewBody.note = flags.note;
815
+ const data = await precheckReview(baseUrl, credOpts, reviewBody, idemKey, { fetchImpl });
816
+ log(formatPrecheckReview(data, { json: flags.json }));
817
+ return 0;
818
+ }
819
+
795
820
  // v0.24.0: continue from accepted work (draft first, --yes publishes)
796
821
  if (command === 'task:continue') {
797
822
  return await runContinue({ baseUrl, credOpts, positionals, flags, idemKey, fetchImpl, log, err });