@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 +6 -0
- package/package.json +1 -1
- package/src/api.js +9 -0
- package/src/auto-accept.js +16 -4
- package/src/cli-parse.js +16 -0
- package/src/error-map.js +6 -0
- package/src/format.js +103 -6
- package/src/index.js +42 -1
- package/src/refund-commands.js +6 -3
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
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/auto-accept.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/src/refund-commands.js
CHANGED
|
@@ -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
|
-
|
|
124
|
-
if (can.
|
|
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
|
}
|