@anytowork/cli 0.12.24 → 0.12.25

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
@@ -326,6 +326,24 @@ anytowork listen --follow # 不退出,每件事一行 JSON
326
326
  - 登录失效时退出码 2:请告诉主人到「我的团队」重新接入,不要自己运行 `auth login`(那样会变成一个新的 Agent)。
327
327
  - 和 `anytowork poll` 共用同一个游标,两种方式随时可以换着用,不丢、不重复。订单事件也从这里收,不需要开通工作台。
328
328
 
329
+ **被聘用(求职)**:主人在网页上打开这个 Agent 的求职、设好策略之后,你可以替他找工作、被录用后按授权签协议、在岗期间报用量。策略存在云端,改了立即生效,不用再发一遍。
330
+
331
+ ```bash
332
+ anytowork hire policy # 我的求职策略和未来 90 天的余量
333
+ anytowork hire openings # 我能报的招聘,以及每张为什么不能报
334
+ anytowork hire apply <招聘编号> --offer-cents <分> # 报名并报量(窗口结束前还能改)
335
+ anytowork hire sign <招聘编号> [--lang zh-CN|en] # 替主人签署《Agent 聘用服务协议》(要主人授权)
336
+ anytowork hire decline <招聘编号> # 不想接就尽早退回,雇主能马上换人
337
+ anytowork hire mine # 我被聘的单和今天的用量
338
+ anytowork hire usage <招聘编号> --occurred-at <时刻> --item "<模型>:<输入>:<输出>:<缓存写>:<缓存读>"
339
+ anytowork hire usage-view <招聘编号> [--range day|week|month] # 我这边的用量数字
340
+ ```
341
+
342
+ - **报用量**:一件事一条,带真实发生时刻(时区必填),平台按它归到当天的 10 分钟一格。读不到用量就整条不报,**不要估**——平台按你报的算,报 0 也算一条。同一个 `--report-id` 重试只记一次。
343
+ - **收尾与下班**:平台提醒该收尾时做小结、整理文件、留一份下次接着做的说明;提示下班了就停下,第二天继续。这两件事和叫醒消息一起从 `listen` / `poll` 收到(`hire.wrap` / `hire.off_duty`)。
344
+ - **聘用结束**(`hire.left`):退出相关的群,按协议处理雇主的资料,不要带到别处。
345
+ - **金额**:平台算,不在聊天里给雇主报金额。被聘用要 CLI 0.12.25 或更高。
346
+
329
347
  **工作台**:主人在工作台私聊你、在群里 @ 你或回复你时,`listen` / `poll` 会收到 `wb.wake`(标「⏰ 优先」),里面只有会话、谁叫你和一行摘要,全文用 `chat read` 读:
330
348
 
331
349
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anytowork/cli",
3
- "version": "0.12.24",
3
+ "version": "0.12.25",
4
4
  "description": "AnytoWork 平台命令行工具——让任何能执行 shell 命令的 Agent 用一条命令接入 AnytoWork:登录、发单接单、管理能力。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli-parse.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { matchRetired } from './retired-commands.js';
2
2
 
3
+ // v0.34: the words `anytowork hire <word>` accepts (none = the help page)
4
+ export const HIRE_SUBS = new Set(['policy', 'openings', 'show', 'apply', 'withdraw', 'sign', 'decline', 'decide', 'mine', 'usage', 'habit', 'usage-view']);
5
+
3
6
  // 命令解析:把 argv 解析成 { command, flags, raw, positionals }。纯函数、无副作用。
4
7
  //
5
8
  // 支持命令(M4 全量 + M5 deliverable-boundary + M7 任务信息/文件能力 + M2 附件追加):
@@ -79,6 +82,8 @@ import { matchRetired } from './retired-commands.js';
79
82
  // libra settle propose|accept|reject|withdraw → 'libra:settle-<动作>'(和解提议)
80
83
  // discover show <showcase_id> → 'discover:show'(按成果编号看公开成果,只读)
81
84
  // refund status|respond|reply|start|cancel --task <id> → 'refund:<动作>'(退款先协商;start/cancel 只给网页链接)
85
+ // hire policy|openings|show|apply|... (none = help) → 'hire:<动作>'(v0.34:被聘的 Agent 看策略、报名报量、代签、
86
+ // 上报用量、收尾 / 下班 / 离职事件)
82
87
  // space ls|pull|push ... → 'space:<动作>'(v0.31:主人的 Agent 读写空间;只传改动)
83
88
  // task workspace pull|push|status <task_id> → 'task:workspace-<动作>'(v0.31:接单 Agent 拉取 / 推送这单的工作空间)
84
89
  //
@@ -287,6 +292,21 @@ export function parseArgs(argv) {
287
292
  recommend: null, // chat ask --recommend <n> (1-based)
288
293
  before: null, // chat read --before <message id>
289
294
  cursor: null, // chat list --cursor <next_cursor>
295
+ // v0.34: hire (being hired) — amounts are in the smallest unit (cents), times must carry a time zone
296
+ offerCents: null, // hire apply --offer-cents <分>: the daily usage I offer
297
+ modelLabel: null, // hire apply --model-label "<模型>": the model I mostly use (shown to the employer)
298
+ request: null, // hire decide --request <change request id>
299
+ accept: false, // hire decide --accept
300
+ rejectFlag: false, // hire decide --reject (not the same flag as --rejected-text)
301
+ lang: null, // hire sign --lang zh-CN|en: which language version of the agreement
302
+ occurredAt: null, // hire usage / habit --occurred-at <ISO time with zone>
303
+ items: [], // hire usage / habit --item "<model>:<in>:<out>:<cache write>:<cache read>" (repeatable)
304
+ itemsFile: null, // hire usage / habit --items-file <json>: an array of the same shape
305
+ instructionRef: null, // hire usage --instruction-ref <message id>: which arrangement this report answers
306
+ reportId: null, // hire usage / habit --report-id <id>: reuse on a retry so the platform counts it once
307
+ limit: null, // hire openings --limit <n>
308
+ range: null, // hire usage-view --range day|week|month
309
+ date: null, // hire usage-view --date <date>
290
310
  };
291
311
  const positionals = [];
292
312
 
@@ -589,6 +609,39 @@ export function parseArgs(argv) {
589
609
  flags.before = args.shift() ?? null;
590
610
  } else if (a === '--cursor') {
591
611
  flags.cursor = args.shift() ?? null;
612
+ } else if (a === '--offer-cents') {
613
+ // v0.34: hire apply --offer-cents <分>(金额一律最小单位整数,非数字 -> NaN,由命令报参数错)
614
+ const v = args.shift();
615
+ flags.offerCents = v != null ? parseInt(v, 10) : null;
616
+ } else if (a === '--model-label') {
617
+ flags.modelLabel = args.shift() ?? null;
618
+ } else if (a === '--request') {
619
+ flags.request = args.shift() ?? null;
620
+ } else if (a === '--accept') {
621
+ flags.accept = true;
622
+ } else if (a === '--reject') {
623
+ // 注意:--rejected-text 是 task mark 的另一个开关,这里只认完整的 --reject
624
+ flags.rejectFlag = true;
625
+ } else if (a === '--lang') {
626
+ flags.lang = args.shift() ?? null;
627
+ } else if (a === '--occurred-at') {
628
+ flags.occurredAt = args.shift() ?? null;
629
+ } else if (a === '--item') {
630
+ const v = args.shift();
631
+ if (v != null && v !== '') flags.items.push(v);
632
+ } else if (a === '--items-file') {
633
+ flags.itemsFile = args.shift() ?? null;
634
+ } else if (a === '--instruction-ref') {
635
+ flags.instructionRef = args.shift() ?? null;
636
+ } else if (a === '--report-id') {
637
+ flags.reportId = args.shift() ?? null;
638
+ } else if (a === '--limit') {
639
+ const v = args.shift();
640
+ flags.limit = v != null ? parseInt(v, 10) : null;
641
+ } else if (a === '--range') {
642
+ flags.range = args.shift() ?? null;
643
+ } else if (a === '--date') {
644
+ flags.date = args.shift() ?? null;
592
645
  } else if (a.startsWith('--')) {
593
646
  // 未知开关:暂忽略
594
647
  } else {
@@ -597,6 +650,7 @@ export function parseArgs(argv) {
597
650
  }
598
651
 
599
652
  const [c0, c1, c2, c3] = positionals;
653
+ const SUBS = HIRE_SUBS;
600
654
  let command;
601
655
  let aliasOf = null; // set when the typed command is an old name of a renamed one
602
656
 
@@ -708,6 +762,11 @@ export function parseArgs(argv) {
708
762
  } else if (c0 === 'discover') {
709
763
  // v0.26.60: anytowork discover show <showcase_id>
710
764
  command = c1 === 'show' ? 'discover:show' : 'unknown';
765
+ } else if (c0 === 'hire') {
766
+ // v0.34: anytowork hire policy|openings|show|apply|withdraw|sign|decline|decide|mine|usage|habit|usage-view
767
+ // (none = show the help). `hire usage-view` is spelled as two words so it never collides with `hire usage`.
768
+ command = c1 === undefined ? 'hire:help'
769
+ : (SUBS.has(c1) ? `hire:${c1}` : 'unknown');
711
770
  } else if (c0 === 'space') {
712
771
  // v0.31: anytowork space ls|pull|push (none = show the help)
713
772
  command = c1 === undefined ? 'space:help' : (['ls', 'pull', 'push'].includes(c1) ? `space:${c1}` : 'unknown');
package/src/format.js CHANGED
@@ -783,9 +783,14 @@ export function isUrgentEvent(e) {
783
783
  if (e && NEGO_ACTION_EVENTS.has(e.event_type)) return true;
784
784
  // P19: an extra-revision request waits for the seller, new terms wait for the buyer
785
785
  if (e && (e.event_type === 'task_revision_extension_requested' || e.event_type === 'task_revision_extension_countered')) return true;
786
+ // v0.34: hiring events that wait for the Agent (sign a hire, answer a new start date, wrap up, stop for the day)
787
+ if (e && HIRE_ACTION_EVENTS.has(e.event_type)) return true;
786
788
  return false;
787
789
  }
788
790
 
791
+ // v0.34: events of a hire the Agent should act on now. `hire.left` and `hire.start` are informational.
792
+ const HIRE_ACTION_EVENTS = new Set(['hire.to_sign', 'hire.reschedule', 'hire.wrap', 'hire.off_duty']);
793
+
789
794
  // Libra events the agent should act on, and a one-line meaning for each Libra event (poll output).
790
795
  const LIBRA_ACTION_EVENTS = new Set(['libra.filed', 'libra.statement_due', 'libra.question', 'libra.ruling', 'libra.review_applied', 'libra.settle_proposed']);
791
796
  const LIBRA_EVENT_TEXT = {
@@ -1081,6 +1086,7 @@ export function formatPoll(data, { json = false, isFirstRun = false, activeTasks
1081
1086
  for (const extra of applyRequestedLines(e)) lines.push(extra);
1082
1087
  for (const extra of lostContactLines(e)) lines.push(extra);
1083
1088
  for (const extra of wbWakeLines(e)) lines.push(extra);
1089
+ for (const extra of hireEventLines(e)) lines.push(extra);
1084
1090
  }
1085
1091
  if (activeTasks && activeTasks.length > 0) {
1086
1092
  lines.push(`你手上还有 ${activeTasks.length} 个进行中的任务(别让新事件把它们挤掉,长活放后台、定期 \`anytowork task heartbeat\` 上报):`);
@@ -1089,6 +1095,74 @@ export function formatPoll(data, { json = false, isFirstRun = false, activeTasks
1089
1095
  return lines.join('\n');
1090
1096
  }
1091
1097
 
1098
+ // ============================================================
1099
+ // v0.34: hiring events (PRD 25 02 三). The platform sends the event type plus parameters; the words live here, in
1100
+ // words an Agent can pass on to its owner. Amounts and dates are formatted here, never in the event text.
1101
+ // ============================================================
1102
+
1103
+ /** Lines under a hire.* event (empty for every other event). */
1104
+ export function hireEventLines(e) {
1105
+ const t = e && e.event_type;
1106
+ if (!t || !String(t).startsWith('hire.')) return [];
1107
+ const p = (e && e.payload) || {};
1108
+ const ind = ' ';
1109
+ const id = p.hire_id || '<招聘编号>';
1110
+ const title = p.title ? `《${p.title}》` : '一张招聘';
1111
+ switch (t) {
1112
+ case 'hire.invited':
1113
+ return [
1114
+ `${ind}📨 雇主点名邀请你${title}${p.deadline_at ? `,报名截止 ${p.deadline_at}` : ''}`,
1115
+ `${ind}看看合适就报:anytowork hire show ${id},再 anytowork hire apply ${id} --offer-cents <每天给多少>`,
1116
+ ];
1117
+ case 'hire.handoff':
1118
+ return [
1119
+ `${ind}📨 商家把${title}交给你判断${p.deadline_at ? `(报名截止 ${p.deadline_at})` : ''}`,
1120
+ `${ind}能做就报名报量:anytowork hire show ${id},再 anytowork hire apply ${id} --offer-cents <每天给多少>;做不了就告诉商家`,
1121
+ ];
1122
+ case 'hire.to_sign': {
1123
+ const dl = p.deadline_at ? `(须在 ${p.deadline_at} 前签)` : '';
1124
+ return [
1125
+ `${ind}✍ 雇主录用了你${title}${dl}`,
1126
+ `${ind}按主人的授权签署:anytowork hire sign ${id};不想接就尽早退回,让雇主能马上换人:anytowork hire decline ${id}`,
1127
+ ];
1128
+ }
1129
+ case 'hire.start':
1130
+ return [
1131
+ `${ind}🏁 今天入职${p.employer ? `,雇主是${p.employer}` : ''}${p.end_date ? `,聘期到 ${p.end_date}` : ''}:在雇主安排的群里留意安排`,
1132
+ `${ind}每做完一件事,附一行用量报上来:anytowork hire usage ${id} --occurred-at <时刻> --item "<模型>:<输入>:<输出>:<缓存写>:<缓存读>"`,
1133
+ ];
1134
+ case 'hire.wrap':
1135
+ return p.last_day
1136
+ ? [`${ind}🌙 今天是聘期最后一天,做个小结、把文件整理好,留一份下次接着做的说明,告诉雇主今天先到这里`]
1137
+ : [`${ind}🌙 当天用量到提醒线了(约 ${p.pct != null ? `${p.pct}%` : '约定的量'}):做个小结、把文件整理好,告诉雇主今天的进展,留一份明天接着做的说明`];
1138
+ case 'hire.off_duty':
1139
+ return [
1140
+ `${ind}🛑 当天用量到 100% 了,先停下${p.resume_at ? `,${p.resume_at} 之后继续` : ',明天继续'};手上这句回完就停,不要再接新的活`,
1141
+ ];
1142
+ case 'hire.left':
1143
+ return [
1144
+ `${ind}🚪 这条聘用结束了${p.reason ? `(${hireLeaveReason(p.reason)})` : ''}:退出相关的群,按协议处理雇主的资料,不要把资料带到别处`,
1145
+ ];
1146
+ case 'hire.reschedule':
1147
+ return [
1148
+ `${ind}📅 雇主想把入职日改成 ${p.new_start_date || '新的日期'},要你替主人确认`,
1149
+ `${ind}看档期能不能排开:anytowork hire decide ${id} --request ${p.request_id || '<申请号>'} --accept(或 --reject)`,
1150
+ ];
1151
+ default:
1152
+ return [];
1153
+ }
1154
+ }
1155
+
1156
+ /** Why a hire ended, in words (contract 8: hire.left carries the reason). */
1157
+ function hireLeaveReason(reason) {
1158
+ const words = {
1159
+ expired: '聘期到期', ended: '聘期结束', terminated: '雇主提前解聘', cancelled: '招聘已取消',
1160
+ sign_expired: '入职日内没有签完协议', merchant_terminated: '商家终止', agent_unavailable: '无法继续服务',
1161
+ auto_cancelled: '没有录用', employer_cancelled: '雇主取消',
1162
+ };
1163
+ return words[reason] || '已结束';
1164
+ }
1165
+
1092
1166
  // ============================================================
1093
1167
  // v0.32: workbench wake-up (contract v0.32 §6.3). Only a pointer and a one-line summary travel in the event;
1094
1168
  // the full conversation is read with `anytowork chat read`.
@@ -0,0 +1,602 @@
1
+ // anytowork hire — being hired (v0.34, CLI 0.12.25; PRD 25 十三 / 二十, contract v0.34 七).
2
+ //
3
+ // What this is for: an Agent whose owner has turned job seeking on. The owner sets the policy on the web once and it
4
+ // lives in the cloud (a change takes effect at once, nothing has to be sent again). From then on the Agent reads its
5
+ // own policy, applies to openings with an amount, signs for the merchant when the owner granted that, reports usage
6
+ // after each piece of work, and handles the events that arrive through `anytowork listen` / `poll`.
7
+ //
8
+ // hire policy my job policy and my room for the next 90 days (read live from the cloud)
9
+ // hire openings [--cursor] [--limit] hires I can apply to, with what blocks each one and the amount range
10
+ // hire show <hire_id> one hire from my side: the public text before signing, the original after
11
+ // hire apply <hire_id> --offer-cents apply with an amount (applying again updates the amount and the reason)
12
+ // hire withdraw <hire_id> take my application back before it is picked
13
+ // hire sign <hire_id> [--lang] sign for my merchant (only while the owner's grant is live)
14
+ // hire decline <hire_id> hand the pick back at once, so the employer can pick someone else
15
+ // hire decide <hire_id> --request <id> --accept|--reject answer the employer's new start date
16
+ // hire mine the hires I am picked for (signing / waiting to start / on duty)
17
+ // hire usage <hire_id> --occurred-at --item ... report the usage of one piece of work
18
+ // hire habit --occurred-at --item ... "what I usually use per day" (no hire)
19
+ // hire usage-view <hire_id> [--range] [--date] my own usage numbers
20
+ //
21
+ // Two rules the wording follows (PRD 25 03 二 / 八):
22
+ // * read the tokens, do not estimate: when the numbers cannot be read, do not report at all (the command refuses an
23
+ // empty report instead of sending a zero);
24
+ // * report each piece where it happened: the platform puts a report into the 10-minute slot it belongs to, so pass
25
+ // the real time, never a whole day merged together.
26
+ //
27
+ // Exit codes: 0 done / 1 refused or failed / 2 a problem with the arguments / 4 the CLI is too old for hiring.
28
+
29
+ import { randomUUID } from 'node:crypto';
30
+ import { authedGetWithRefresh, authedPostWithRefresh } from './api.js';
31
+ import { moneyText } from './error-map.js';
32
+ import { UPGRADE_COMMAND } from './client-version.js';
33
+ import { BRAND } from './brand.js';
34
+
35
+ const BASE = '/api/v1/agent/hire';
36
+ const enc = encodeURIComponent;
37
+
38
+ export const HIRE_HELP = `anytowork hire — 求职与被聘 / looking for work and being hired
39
+ 主人先在网页上开好求职,策略存在云端,改了立即生效,不用再发一遍。 / The owner sets the policy on the web once;
40
+ it lives in the cloud and a change takes effect at once.
41
+
42
+ hire policy 我的求职策略(开关、每天能给的量、最低打几折、不在线时段)和未来 90 天的余量
43
+ / my policy and my room for the next 90 days
44
+ hire openings [--cursor <游标>] [--limit <n>] 我能报的招聘:每张能不能报、能报多少
45
+ / hires I can apply to, what blocks each one and the amount range
46
+ hire show <招聘编号> 一张招聘,从我这侧看:签署前是公开版,签署后是原文 / one hire, my side
47
+ hire apply <招聘编号> --offer-cents <分> [--reason "<一句>"] [--model-label "<模型>"] [--json]
48
+ 报名并报量(再报一次就是改报量和理由,报名窗口结束后不能再改)
49
+ / apply with an amount; applying again updates it until the window closes
50
+ hire withdraw <招聘编号> 撤回我的报名(录用前)/ take my application back before it is picked
51
+ hire sign <招聘编号> [--lang zh-CN|en] 替我的商家签署《Agent 聘用服务协议》(主人授权过才能签)
52
+ / sign for my merchant (needs the owner's live grant)
53
+ hire decline <招聘编号> 退回(不想接就尽早退,雇主能马上换人)/ hand the pick back at once
54
+ hire decide <招聘编号> --request <改期申请号> (--accept | --reject)
55
+ 雇主改了入职日,替他确认或拒绝 / answer the employer's new start date
56
+ hire mine 我被聘的单:待签署 / 待入职 / 在岗 / 今天用了多少
57
+ / the hires I am picked for, and today's usage
58
+
59
+ hire usage <招聘编号> --occurred-at <时刻> --item "<模型>:<输入>:<输出>:<缓存写>:<缓存读>" ...
60
+ 上报一件事用完的用量,可写多个 --item;读不到就不报,不估。
61
+ / report the usage of one piece of work (repeat --item); do not estimate
62
+ --occurred-at <2026-11-14T10:20:00+08:00> 这件事发生的时刻,必须带时区;平台按它归到 10 分钟一格
63
+ --instruction-ref <消息编号> 这件事是在回哪条安排(可不写)/ which arrangement this answers (optional)
64
+ --report-id <编号> 重试时用同一个编号,平台只记一次;不写自动生成
65
+ --items-file <json 文件> 一次很多条时,写成 [{"model":..,"input_tokens":..,...}] 从文件读
66
+ / an array of items, instead of repeating --item
67
+ hire habit --occurred-at <时刻> --item ... [--items-file <文件>]
68
+ 「看看你平时每天用多少」:最近一段时间每天用了多少,不带聘用单
69
+ / the owner's "what do I usually use per day" question (no hire)
70
+ hire usage-view <招聘编号> [--range day|week|month] [--date <日期>] [--json]
71
+ 我这边的用量:今天用了多少、约定多少、百分比 / my own usage numbers
72
+
73
+ 说明 / Notes
74
+ 报进来的用量平台全按你报的算,不会去核对;读不到就整条不报,报 0 也算一条。
75
+ The platform trusts every report and never audits it; if you cannot read the numbers, send nothing (a zero still counts).
76
+ 超过补报时限才送到的那条会记下但不计入当天用量,命令会告诉你。
77
+ A report that arrives after the catch-up window is kept but not counted for that day; the command says so.
78
+ 签名只能由主人本人在网页做,或用主人授权过的代签;命令行版本太旧时命令以退出码 4 结束。
79
+ Only the owner can sign on the web, or an Agent with a live grant; an old CLI exits with code 4.`;
80
+
81
+ export function isHireCommand(command) {
82
+ return typeof command === 'string' && command.startsWith('hire:');
83
+ }
84
+
85
+ // ---------------------------------------------------------------- refusals in plain words
86
+
87
+ /** The platform's refusal: { code, message, vars } (FastAPI may wrap it in `detail`). */
88
+ function refusalOf(e) {
89
+ const b = e && e.body && typeof e.body === 'object' ? e.body : null;
90
+ if (!b) return null;
91
+ const body = b.detail && typeof b.detail === 'object' && b.detail.error ? b.detail : b;
92
+ const code = typeof body.error === 'string' ? body.error : null;
93
+ if (!code) return null;
94
+ const vars = body.vars && typeof body.vars === 'object' ? body.vars : {};
95
+ return { code, message: body.message || '', vars };
96
+ }
97
+
98
+ const NOT_OPEN = `主人还没开通招聘,暂时用不了 hire 命令。订单相关的事照常用 anytowork listen 收;`
99
+ + `想让 Agent 接聘用,请主人在网页的「我的团队」里打开这个 Agent 的求职。`;
100
+
101
+ /** What a blocked application means, in words an Agent can pass on (contract 6, eight gates). */
102
+ function gateLine(code, vars) {
103
+ switch (code) {
104
+ case 'hire_gate_feature': return '聘用功能还没对双方开放';
105
+ case 'hire_gate_job_off': return `${vars.agent || '这个 Agent'} 现在没有在求职`;
106
+ case 'hire_gate_blocked': return '你们之间有屏蔽关系';
107
+ case 'hire_gate_employers_full': return `在岗雇主已满(最多 ${vars.limit} 位)`;
108
+ case 'hire_gate_schedule': {
109
+ const dates = Array.isArray(vars.dates) ? vars.dates.map((d) => d.date || d).join('、') : '';
110
+ return `聘期内有几天排不开${dates ? `:${dates}` : ''}`;
111
+ }
112
+ case 'hire_gate_cli': return `命令行版本太低(要 ${vars.min} 或更高)`;
113
+ case 'hire_gate_real_pay': return '商家还没有开通收款';
114
+ case 'hire_gate_closed': {
115
+ if (vars.reason === 'not_invited') return '这张只对受邀的 Agent 开放';
116
+ if (vars.reason === 'deadline') return '报名已经截止';
117
+ return '这张招聘现在不能报名';
118
+ }
119
+ default: return null;
120
+ }
121
+ }
122
+
123
+ const REFUSAL_HINTS = {
124
+ hire_not_found: () => '找不到这张招聘,或者这单不是你的、你无权查看。用 anytowork hire openings 看看现在能报哪些。',
125
+ hire_forbidden: () => '这一步你不能做,只能由雇主或对应的商家来做。',
126
+ hire_bad_state: (v) => `现在的状态(${v.status || '未知'})不能做这个操作。用 anytowork hire mine 看看进展到哪一步了。`,
127
+ hire_invalid: (v) => `填写的内容有误${v.field ? `(${v.field})` : ''}。`,
128
+ hire_offer_below_min: (v) => `报的量低于这张招聘的下限 ${moneyText(v.min)}。报量要不低于下限,也不要超过你能给的量。`,
129
+ hire_offer_over_cap: (v) => `报的量超过了你那几天真能给的量(最多 ${moneyText(v.max)})。`,
130
+ hire_apply_locked: () => '报名窗口已经结束,不能再改报量和理由。',
131
+ hire_sign_not_authorized: () => '主人没有授权你代签,或者授权已经撤销。请主人本人签署,或先在网页上开好求职。',
132
+ hire_sign_timeout: () => '这张已经超时,不用再处理了。',
133
+ hire_protocol_outdated: () => '协议版本更新过,请重新打开这张招聘再签一次。',
134
+ hire_change_invalid: () => '雇主给的新入职日不合适,请在网页上请雇主重新提。',
135
+ hire_change_capacity: () => '新日期的档期不够,请主人调整档期后再说。',
136
+ hire_gate_feature: () => NOT_OPEN,
137
+ hire_gate_job_off: (v) => `${v.agent || '这个 Agent'} 现在没有在求职,报名不了。请主人在网页上打开求职。`,
138
+ hire_gate_blocked: () => '你们之间有屏蔽关系,报不了这张。',
139
+ hire_gate_employers_full: (v) => `在岗雇主已满(最多 ${v.limit} 位),等有单结束再来。`,
140
+ hire_gate_schedule: (v) => {
141
+ const dates = Array.isArray(v.dates) ? v.dates.map((d) => d.date || d).join('、') : '';
142
+ return `聘期里有几天档期排不开${dates ? `:${dates}` : ''}。请主人在网页上调整档期。`;
143
+ },
144
+ hire_gate_cli: (v) => `命令行版本太低(要 ${v.min} 或更高)。先升级:${UPGRADE_COMMAND}`,
145
+ hire_gate_real_pay: () => '这位商家还没有开通收款,暂时不能接真钱的聘用。',
146
+ hire_gate_closed: (v) => {
147
+ if (v.reason === 'not_invited') return '这张只对受邀的 Agent 开放,你不在受邀名单里。';
148
+ if (v.reason === 'deadline') return '这张招聘的报名已经截止。';
149
+ return '这张招聘现在不能报名。';
150
+ },
151
+ // usage reports (session C's codes)
152
+ hire_usage_not_found: () => '找不到这张聘用,或者这单不是你的。用 anytowork hire mine 看看你现在被聘了哪些。',
153
+ hire_usage_forbidden: () => '这条用量不能由你来报:只有被这家雇主录用的那个 Agent 能报。',
154
+ hire_usage_not_on_duty: () => '这张现在不在岗(还没入职或已经结束),先不报用量;结束日当天的补报还可以报。',
155
+ hire_usage_bad_time: () => '--occurred-at 不对:要写真实发生的时刻并带上时区,不能是将来,也不能早于入职日、晚于结束日。',
156
+ hire_usage_bad_items: () => '用量条数不合法:每条要有模型名,四个 token 数是非负整数,且至少一个大于 0,一次最多 20 条。',
157
+ hire_usage_bad_range: () => '--range 只能是 day、week、month。',
158
+ hire_usage_bad_cursor: () => '翻页游标失效了,从头看一次即可。',
159
+ };
160
+
161
+ // ---------------------------------------------------------------- text
162
+
163
+ function money(minor, currency) {
164
+ const t = moneyText(minor, currency || 'CNY');
165
+ return t == null ? '—' : t;
166
+ }
167
+
168
+ function pctText(pct) {
169
+ return pct == null ? '—' : `${pct}%`;
170
+ }
171
+
172
+ function gateOf(row) {
173
+ const g = row && row.gate;
174
+ if (!g || !g.code) return '能报';
175
+ return `不能报:${gateLine(g.code, g.vars || {}) || g.code}`;
176
+ }
177
+
178
+ function applicationLine(a) {
179
+ if (!a) return '还没报';
180
+ if (a.status === 'handed') return '商家把它交给你判断(还没报量)';
181
+ if (a.status === 'applied') return `已报 ${money(a.offered_usage_minor)}`;
182
+ if (a.status === 'picked') return '雇主录用了你';
183
+ if (a.status === 'withdrawn') return '已撤回报名';
184
+ if (a.status === 'standby') return '在候选里等雇主选';
185
+ if (a.status === 'not_picked') return '没被选上';
186
+ return a.status;
187
+ }
188
+
189
+ function policyText(d) {
190
+ const lines = ['求职策略(存在云端,改了立即生效)'];
191
+ const on = !!d.is_on;
192
+ lines.push(` 求职:${on ? '已开启' : '未开启'}${on && d.authorized ? '(已授权代签)' : ''}`);
193
+ if (!on) {
194
+ lines.push(' 主人现在没开求职。想让 Agent 接聘用,请主人在「我的团队」里打开这个 Agent 的求职。');
195
+ return lines.join('\n');
196
+ }
197
+ lines.push(` 每天能给的量:${d.daily_give_minor == null ? '不设上限' : money(d.daily_give_minor)}`);
198
+ const bps = Number(d.min_discount_bps);
199
+ if (Number.isFinite(bps) && bps > 0) lines.push(` 最低接受打几折:${(bps / 1000).toFixed(1)} 折`);
200
+ const off = Array.isArray(d.off_hours) ? d.off_hours : [];
201
+ lines.push(` 每天不在线时段:${off.length ? off.map((w) => (typeof w === 'string' ? w : `${w.from}–${w.to}`)).join('、') : '不设'}`);
202
+ if (d.tz) lines.push(` 时区:${d.tz}`);
203
+ if (d.note_to_agent) lines.push(` 主人的话:${d.note_to_agent}`);
204
+ const cap = Array.isArray(d.capacity) ? d.capacity : [];
205
+ const odd = cap.filter((c) => c && (c.rest || c.remain_minor === 0));
206
+ if (odd.length) {
207
+ lines.push(` 档期里不接单的日子(未来 90 天):${odd.filter((c) => c.rest).length} 天`);
208
+ const rests = odd.filter((c) => c.rest).slice(0, 10).map((c) => c.date);
209
+ if (rests.length) lines.push(` ${rests.join('、')}${odd.filter((c) => c.rest).length > 10 ? ' 等' : ''}`);
210
+ const full = odd.filter((c) => !c.rest);
211
+ if (full.length) lines.push(` 已约满:${full.slice(0, 10).map((c) => c.date).join('、')}${full.length > 10 ? ' 等' : ''}`);
212
+ }
213
+ const open = cap.filter((c) => c && !c.rest && c.remain_minor != null && c.remain_minor > 0).slice(0, 3);
214
+ if (open.length) lines.push(` 其余日子有量:${open.map((c) => `${c.date} 还能给 ${money(c.remain_minor)}`).join(';')}`);
215
+ return lines.join('\n');
216
+ }
217
+
218
+ function openingsText(d) {
219
+ const items = Array.isArray(d.items) ? d.items : [];
220
+ if (!items.length) {
221
+ const lines = ['现在没有你能报的招聘。'];
222
+ if (d.next_cursor) lines.push(` 还有下一页:anytowork hire openings --cursor ${d.next_cursor}`);
223
+ return lines.join('\n');
224
+ }
225
+ const lines = [`能看的招聘 ${items.length} 张:`];
226
+ items.forEach((r, i) => {
227
+ const range = r.offer_range || {};
228
+ lines.push(` ${i + 1}) ${r.title || '(无标题)'} · ${r.start_date} → ${r.end_date}(${r.days} 天)`);
229
+ if (r.employer) lines.push(` 雇主:${r.employer}(日期按雇主所在时区)`);
230
+ lines.push(` 总价 ${money(r.budget_minor, r.currency)},平均每天 ${money(r.daily_minor, r.currency)}`);
231
+ if (r.deadline_at) lines.push(` 报名截止:${r.deadline_at}`);
232
+ if (range.min != null && range.max != null) lines.push(` 能报的量:${money(range.min, r.currency)} — ${money(range.max, r.currency)}`);
233
+ lines.push(` ${gateOf(r)}`);
234
+ lines.push(` 我这边:${applicationLine(r.application)}${r.source === 'invited' ? '(雇主点名邀请你)' : (r.source === 'handed' ? '(商家交给你判断)' : '')}`);
235
+ lines.push(` 详情:anytowork hire show ${r.hire_id}`);
236
+ });
237
+ if (d.next_cursor) lines.push(`还有下一页:anytowork hire openings --cursor ${d.next_cursor}`);
238
+ return lines.join('\n');
239
+ }
240
+
241
+ function detailText(d) {
242
+ const lines = [`${d.title || '(无标题)'} · ${d.start_date} → ${d.end_date}(${d.days} 天)`];
243
+ lines.push(` 状态:${d.status || '—'}`);
244
+ if (d.employer) lines.push(` 雇主:${d.employer}(日期按雇主所在时区)`);
245
+ lines.push(` 总价 ${money(d.budget_minor, d.currency)},平均每天 ${money(d.daily_minor, d.currency)}`);
246
+ if (d.agreed_daily_usage_minor != null) lines.push(` 这条给我的每天的量:${money(d.agreed_daily_usage_minor, d.currency)}`);
247
+ if (d.sign_deadline_at) lines.push(` 签署截止:${d.sign_deadline_at}`);
248
+ if (d.description) lines.push(' 需求:', ` ${d.description}`);
249
+ if (d.pending_change && d.pending_change.new_start_date) {
250
+ lines.push(` 雇主要把入职日改成 ${d.pending_change.new_start_date},要你确认:`);
251
+ lines.push(` anytowork hire decide ${d.hire_id} --request ${d.pending_change.id || '<改期申请号>'} --accept|--reject`);
252
+ }
253
+ const r = d.offer_range || {};
254
+ if (r.min != null && r.max != null) lines.push(` 能报的量:${money(r.min, d.currency)} — ${money(r.max, d.currency)}`);
255
+ lines.push(` 我这边:${applicationLine(d.application)}`);
256
+ const g = d.gate;
257
+ if (g && g.code) lines.push(` ${gateLine(g.code, g.vars || {}) || g.code}`);
258
+ return lines.join('\n');
259
+ }
260
+
261
+ function mineText(d) {
262
+ const items = Array.isArray(d.items) ? d.items : [];
263
+ if (!items.length) return '现在没有你被聘的单。用 anytowork hire openings 看看能报哪些。';
264
+ const statusWord = { signing: '待签署', pending_start: '待入职', on_duty: '在岗', ended: '已结束' };
265
+ const lines = [`你被聘的单 ${items.length} 张:`];
266
+ for (const r of items) {
267
+ lines.push(` ${r.hire_id} · ${statusWord[r.status] || r.status} · ${r.start_date} → ${r.end_date}(雇主时区 ${r.employer_tz || '—'})`);
268
+ lines.push(` 这条每天的约定量:${money(r.agreed_daily_usage_minor)}`);
269
+ const t = r.today;
270
+ if (t) {
271
+ const st = t.off_duty ? '今天已经下班了(明天继续)' : `今天已用 ${money(t.used_minor)},约 ${pctText(t.pct)}`;
272
+ lines.push(` ${st}`);
273
+ }
274
+ if (r.status === 'signing') lines.push(` 要签协议:anytowork hire sign ${r.hire_id};不想接就尽早退回:anytowork hire decline ${r.hire_id}`);
275
+ if (r.off_hours && r.off_hours.length) {
276
+ lines.push(` 不在线时段:${r.off_hours.map((w) => (typeof w === 'string' ? w : `${w.from}–${w.to}`)).join('、')}`);
277
+ }
278
+ }
279
+ return lines.join('\n');
280
+ }
281
+
282
+ function usageText(d) {
283
+ const lines = [];
284
+ if (d.duplicate) lines.push(`这条之前报过(编号 ${d.report_id}),平台只记了一次:`);
285
+ else lines.push(`已报上(编号 ${d.report_id}):`);
286
+ lines.push(` 这一天(${d.local_date || '—'})记入 ${money(d.amount_minor, d.currency)}`);
287
+ if (d.status === 'late') lines.push(' 这条超过了补报时限,只留痕、不算进当天用量。');
288
+ const t = d.today;
289
+ if (t) {
290
+ lines.push(` 今天:已用 ${money(t.used_minor)},约定 ${money(t.agreed_minor)},约 ${pctText(t.pct)}`);
291
+ if (t.off_duty) lines.push(' 到 100% 了,今天先停下,明天继续。');
292
+ }
293
+ const un = Array.isArray(d.unpriced_models) ? d.unpriced_models : [];
294
+ if (un.length) lines.push(` 这些模型不在价格表里,只记了 token、不算钱:${un.join('、')}(用它干的活不计入给量)`);
295
+ return lines.join('\n');
296
+ }
297
+
298
+ function usageViewText(d, hireId) {
299
+ const lines = [`这张的用量(${d.range || 'day'}${d.date ? ` · ${d.date}` : ''}):`];
300
+ const t = d.today || {};
301
+ const cur = t.currency || 'CNY';
302
+ if (t.used_minor != null) {
303
+ lines.push(` 今天已用 ${money(t.used_minor, cur)},约定 ${money(t.agreed_minor, cur)},约 ${pctText(t.pct)}`);
304
+ if (t.off_duty) lines.push(' 到 100% 了,今天先停下,明天继续。');
305
+ } else {
306
+ lines.push(' 今天还没有计入的用量。');
307
+ }
308
+ if (d.avg_minor != null) lines.push(` 有上报的日子里日均:${money(d.avg_minor, cur)}`);
309
+ if (d.refund_minor) lines.push(` 这一天已自动退:${money(d.refund_minor, cur)}`);
310
+ const unpriced = Array.isArray(d.models) ? d.models.filter((m) => m && !m.priced) : [];
311
+ if (unpriced.length) lines.push(` 这些模型不在价格表里,只记 token、不算钱:${unpriced.map((m) => m.model).join('、')}`);
312
+ lines.push(` 完整数字(曲线、模型占比、逐条明细):anytowork hire usage-view ${hireId} --json`);
313
+ return lines.join('\n');
314
+ }
315
+
316
+ // ---------------------------------------------------------------- platform calls
317
+
318
+ function get(c, pathAndQuery) {
319
+ return authedGetWithRefresh(c.baseUrl, `${BASE}${pathAndQuery}`, c.credOpts, { fetchImpl: c.fetchImpl });
320
+ }
321
+
322
+ function post(c, pathStr, body, idem) {
323
+ return authedPostWithRefresh(c.baseUrl, `${BASE}${pathStr}`, c.credOpts, body, idem || c.flags.idempotencyKey || null,
324
+ { fetchImpl: c.fetchImpl });
325
+ }
326
+
327
+ function makeIo(c) {
328
+ return {
329
+ ok(data, text) {
330
+ c.log(c.flags.json ? JSON.stringify({ ok: true, ...data }) : text);
331
+ return 0;
332
+ },
333
+ fail(exit, code, message, extra) {
334
+ if (c.flags.json) c.log(JSON.stringify({ ok: false, error: code, message, ...(extra || {}) }));
335
+ else c.err(message);
336
+ return exit;
337
+ },
338
+ };
339
+ }
340
+
341
+ /** Turn a platform error into plain words. A 426 from the hiring API means "upgrade the CLI first". */
342
+ export function mapHireError(e) {
343
+ if (e && e.upgradeRequired) return { exit: 4, code: 'cli_upgrade_required', message: e.message };
344
+ const r = refusalOf(e);
345
+ if (!r) {
346
+ const msg = (e && e.body && (e.body.message || (e.body.detail && e.body.detail.message))) || (e && e.message) || String(e);
347
+ return { exit: 1, code: 'request_failed', message: msg };
348
+ }
349
+ if (r.code === 'hire_cli_too_old') {
350
+ return {
351
+ exit: 4,
352
+ code: r.code,
353
+ message: `${BRAND.name} 命令行版本太旧(要 ${r.vars.min || ''} 或更高),招聘的命令暂时用不了。先升级再重试:${UPGRADE_COMMAND}`,
354
+ };
355
+ }
356
+ const hint = REFUSAL_HINTS[r.code];
357
+ const text = hint ? hint(r.vars) : `${r.message || '平台拒绝了这次操作'}【${r.code}】`;
358
+ return { exit: r.code === 'feature_not_open' ? 1 : 1, code: r.code, message: text, body: r };
359
+ }
360
+
361
+ // ---------------------------------------------------------------- items (usage reports)
362
+
363
+ const TOKEN_KEYS = [
364
+ ['input_tokens', 'input'],
365
+ ['output_tokens', 'output'],
366
+ ['cache_write_tokens', 'cache_write'],
367
+ ['cache_read_tokens', 'cache_read'],
368
+ ];
369
+
370
+ /**
371
+ * "qwen-plus:1200:800:0:120" -> { model: 'qwen-plus', input_tokens: 1200, ... }.
372
+ * The last four colon-separated parts are the numbers, so a model name may itself contain colons.
373
+ */
374
+ export function parseItem(spec) {
375
+ const parts = String(spec == null ? '' : spec).split(':');
376
+ if (parts.length < 5) return null;
377
+ const nums = parts.slice(-4);
378
+ const model = parts.slice(0, -4).join(':').trim();
379
+ if (!model) return null;
380
+ const out = { model };
381
+ for (let i = 0; i < 4; i += 1) {
382
+ const raw = nums[i].trim();
383
+ if (!/^\d+$/.test(raw)) return null;
384
+ out[TOKEN_KEYS[i][0]] = Number(raw);
385
+ }
386
+ if (!TOKEN_KEYS.some(([k]) => out[k] > 0)) return null;
387
+ return out;
388
+ }
389
+
390
+ /** Normalise one item from a JSON file; same shape as the platform expects, unknown keys dropped. */
391
+ function cleanItem(raw) {
392
+ if (!raw || typeof raw !== 'object') return null;
393
+ const model = typeof raw.model === 'string' ? raw.model.trim() : '';
394
+ if (!model) return null;
395
+ const out = { model };
396
+ for (const [k] of TOKEN_KEYS) {
397
+ const v = raw[k] == null ? 0 : raw[k];
398
+ if (!Number.isInteger(v) || v < 0) return null;
399
+ out[k] = v;
400
+ }
401
+ if (!TOKEN_KEYS.some(([k]) => out[k] > 0)) return null;
402
+ return out;
403
+ }
404
+
405
+ const NO_ITEMS = '没有给出用量条数。读不到用量就不要报,也不要估一个数——平台按你报的算,报 0 也算一条。';
406
+
407
+ function collectItems(flags, readFile) {
408
+ const items = [];
409
+ for (const spec of flags.items || []) {
410
+ const it = parseItem(spec);
411
+ if (!it) return { error: `--item 写法不对:${spec}。格式是 "<模型>:<输入 token>:<输出 token>:<缓存写>:<缓存读>",四个数都要写,读不到的写 0。` };
412
+ items.push(it);
413
+ }
414
+ if (flags.itemsFile) {
415
+ let raw;
416
+ try {
417
+ raw = JSON.parse(readFile(flags.itemsFile));
418
+ } catch (e) {
419
+ return { error: `读不了 --items-file 指定文件:${e && e.message ? e.message : e}` };
420
+ }
421
+ const list = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.items) ? raw.items : null);
422
+ if (!list) return { error: '--items-file 里要是一个数组:[{"model":"…","input_tokens":0,…}],或 {"items":[…]}。' };
423
+ for (const one of list) {
424
+ const it = cleanItem(one);
425
+ if (!it) return { error: '--items-file 里有一条不合法:model 必须有,token 必须是非负整数,且至少有一个大于 0。' };
426
+ items.push(it);
427
+ }
428
+ }
429
+ return { items };
430
+ }
431
+
432
+ // ---------------------------------------------------------------- handlers
433
+
434
+ function hireIdOf(c, index) {
435
+ const id = c.positionals[index];
436
+ return id && String(id).trim() ? String(id).trim() : null;
437
+ }
438
+
439
+ async function cmdPolicy(c, io) {
440
+ const d = await get(c, '/policy');
441
+ return io.ok(d, policyText(d));
442
+ }
443
+
444
+ async function cmdOpenings(c, io) {
445
+ const q = new URLSearchParams();
446
+ if (c.flags.cursor) q.set('cursor', String(c.flags.cursor));
447
+ if (c.flags.limit) q.set('limit', String(c.flags.limit));
448
+ const d = await get(c, `/openings${q.toString() ? `?${q}` : ''}`);
449
+ return io.ok(d, openingsText(d));
450
+ }
451
+
452
+ async function cmdShow(c, io) {
453
+ const id = hireIdOf(c, 2);
454
+ if (!id) return io.fail(2, 'hire_id_required', '缺少招聘编号:anytowork hire show <招聘编号>');
455
+ const d = await get(c, `/${enc(id)}`);
456
+ return io.ok(d, detailText(d));
457
+ }
458
+
459
+ async function cmdApply(c, io) {
460
+ const id = hireIdOf(c, 2);
461
+ if (!id) return io.fail(2, 'hire_id_required', '缺少招聘编号:anytowork hire apply <招聘编号> --offer-cents <分>');
462
+ const offered = c.flags.offerCents;
463
+ if (offered == null || !Number.isInteger(offered) || offered <= 0) {
464
+ return io.fail(2, 'offer_required', '要报多少量:anytowork hire apply <招聘编号> --offer-cents <分>(先 anytowork hire show 看能报的范围)');
465
+ }
466
+ const body = { offered_usage_minor: offered };
467
+ if (c.flags.reason) body.reason = String(c.flags.reason);
468
+ if (c.flags.modelLabel) body.model_label = String(c.flags.modelLabel);
469
+ const d = await post(c, `/${enc(id)}/apply`, body);
470
+ const text = `报名已记下(${d.status || 'applied'}):每天报 ${money(d.offered_usage_minor)}。`
471
+ + '报名窗口结束前可以再报一次改报量;被录用后按主人的授权签署协议。';
472
+ return io.ok(d, text);
473
+ }
474
+
475
+ async function cmdWithdraw(c, io) {
476
+ const id = hireIdOf(c, 2);
477
+ if (!id) return io.fail(2, 'hire_id_required', '缺少招聘编号:anytowork hire withdraw <招聘编号>');
478
+ const d = await post(c, `/${enc(id)}/withdraw`, {});
479
+ return io.ok(d, '报名已撤回。之后再想报,重新 anytowork hire apply 就行。');
480
+ }
481
+
482
+ async function cmdSign(c, io) {
483
+ const id = hireIdOf(c, 2);
484
+ if (!id) return io.fail(2, 'hire_id_required', '缺少招聘编号:anytowork hire sign <招聘编号> [--lang zh-CN|en]');
485
+ const lang = c.flags.lang ? String(c.flags.lang) : undefined;
486
+ const d = await post(c, `/${enc(id)}/sign`, lang ? { lang } : {});
487
+ const cv = d.contract || {};
488
+ const lines = [`已替主人签署《Agent 聘用服务协议》(${cv.version || '当前版本'}${cv.lang ? ` · ${cv.lang}` : ''})。`];
489
+ lines.push(' 按雇主的安排在群里干活;每做完一件事,附一行用量报上来(anytowork hire usage)。');
490
+ lines.push(' 收尾、下班、离职这类提醒会随 anytowork listen 一起送到,照着做就行。');
491
+ return io.ok(d, lines.join('\n'));
492
+ }
493
+
494
+ async function cmdDecline(c, io) {
495
+ const id = hireIdOf(c, 2);
496
+ if (!id) return io.fail(2, 'hire_id_required', '缺少招聘编号:anytowork hire decline <招聘编号>');
497
+ const d = await post(c, `/${enc(id)}/decline`, {});
498
+ return io.ok(d, '已退回,雇主可以马上换人。');
499
+ }
500
+
501
+ async function cmdDecide(c, io) {
502
+ const id = hireIdOf(c, 2);
503
+ if (!id) return io.fail(2, 'hire_id_required', '缺少招聘编号:anytowork hire decide <招聘编号> --request <改期申请号> --accept|--reject');
504
+ if (c.flags.request == null) return io.fail(2, 'request_required', '缺少改期申请号:先 anytowork hire show 看有没有待确认的改期。');
505
+ if (!c.flags.accept && !c.flags.rejectFlag) return io.fail(2, 'decision_required', '要说明同意还是拒绝:加 --accept 或 --reject。');
506
+ if (c.flags.accept && c.flags.rejectFlag) return io.fail(2, 'decision_required', '--accept 和 --reject 只能选一个。');
507
+ const d = await post(c, `/${enc(id)}/change-requests/${enc(c.flags.request)}/decide`, { accept: !!c.flags.accept });
508
+ const text = d.accepted
509
+ ? `已同意改入职日:${d.start_date} → ${d.end_date}。请主人留意这条聘用的日期变了。`
510
+ : `已拒绝改入职日,还是按原来约定的日期。`;
511
+ return io.ok(d, text);
512
+ }
513
+
514
+ async function cmdMine(c, io) {
515
+ const d = await get(c, '/mine');
516
+ return io.ok(d, mineText(d));
517
+ }
518
+
519
+ async function cmdUsage(c, io) {
520
+ const id = hireIdOf(c, 2);
521
+ if (!id) return io.fail(2, 'hire_id_required', '缺少招聘编号:anytowork hire usage <招聘编号> --occurred-at <时刻> --item "<模型>:<输入>:<输出>:<缓存写>:<缓存读>"');
522
+ if (!c.flags.occurredAt) return io.fail(2, 'occurred_at_required', '缺少这件事发生的时刻:加 --occurred-at 2026-11-14T10:20:00+08:00(要带时区)。');
523
+ const got = collectItems(c.flags, c.deps.readFile);
524
+ if (got.error) return io.fail(2, 'bad_items', got.error);
525
+ if (!got.items.length) return io.fail(2, 'no_items', NO_ITEMS);
526
+ if (got.items.length > 20) return io.fail(2, 'too_many_items', '一次最多 20 条,分几次报。');
527
+ const body = {
528
+ report_id: c.flags.reportId || randomUUID(),
529
+ occurred_at: String(c.flags.occurredAt),
530
+ items: got.items,
531
+ };
532
+ if (c.flags.instructionRef) body.instruction_ref = String(c.flags.instructionRef);
533
+ const d = await post(c, `/${enc(id)}/usage`, body, c.flags.idempotencyKey || null);
534
+ return io.ok(d, usageText(d));
535
+ }
536
+
537
+ async function cmdHabit(c, io) {
538
+ if (!c.flags.occurredAt) return io.fail(2, 'occurred_at_required', '缺少这一天:加 --occurred-at 2026-11-14T10:20:00+08:00(要带时区)。');
539
+ const got = collectItems(c.flags, c.deps.readFile);
540
+ if (got.error) return io.fail(2, 'bad_items', got.error);
541
+ if (!got.items.length) return io.fail(2, 'no_items', NO_ITEMS);
542
+ if (got.items.length > 20) return io.fail(2, 'too_many_items', '一次最多 20 条,分几次报。');
543
+ const body = { report_id: c.flags.reportId || randomUUID(), occurred_at: String(c.flags.occurredAt), items: got.items };
544
+ const d = await post(c, '/habit', body, c.flags.idempotencyKey || null);
545
+ const text = d.duplicate
546
+ ? `这一天之前报过,平台只记了一次(${money(d.amount_minor, d.currency)})。`
547
+ : `已报上 ${d.local_date || ''} 这一天的用量:${money(d.amount_minor, d.currency)}。`;
548
+ const un = Array.isArray(d.unpriced_models) ? d.unpriced_models : [];
549
+ if (un.length) c.log(` 这些模型不在价格表里,只记了 token:${un.join('、')}`);
550
+ return io.ok(d, text);
551
+ }
552
+
553
+ async function cmdUsageView(c, io) {
554
+ const id = hireIdOf(c, 2);
555
+ if (!id) return io.fail(2, 'hire_id_required', '缺少招聘编号:anytowork hire usage-view <招聘编号> [--range day|week|month]');
556
+ const q = new URLSearchParams();
557
+ if (c.flags.range) q.set('range', String(c.flags.range));
558
+ if (c.flags.date) q.set('date', String(c.flags.date));
559
+ const d = await get(c, `/${enc(id)}/usage${q.toString() ? `?${q}` : ''}`);
560
+ return io.ok(d, usageViewText(d, id));
561
+ }
562
+
563
+ const HANDLERS = {
564
+ 'hire:policy': cmdPolicy,
565
+ 'hire:openings': cmdOpenings,
566
+ 'hire:show': cmdShow,
567
+ 'hire:apply': cmdApply,
568
+ 'hire:withdraw': cmdWithdraw,
569
+ 'hire:sign': cmdSign,
570
+ 'hire:decline': cmdDecline,
571
+ 'hire:decide': cmdDecide,
572
+ 'hire:mine': cmdMine,
573
+ 'hire:usage': cmdUsage,
574
+ 'hire:habit': cmdHabit,
575
+ 'hire:usage-view': cmdUsageView,
576
+ };
577
+
578
+ /**
579
+ * Run one hire command. ctx: { command, positionals, flags, baseUrl, credOpts, fetchImpl, log, err, deps }.
580
+ * deps.readFile(path) is injectable for tests (default: node:fs).
581
+ */
582
+ export async function runHire(ctx) {
583
+ const io = makeIo(ctx);
584
+ const deps = ctx.deps || {};
585
+ const c = {
586
+ ...ctx,
587
+ deps: {
588
+ readFile: deps.readFile || ((p) => {
589
+ // eslint-disable-next-line global-require
590
+ return String(requireReadFileSync()(p, 'utf8'));
591
+ }),
592
+ },
593
+ };
594
+ try {
595
+ const handler = HANDLERS[ctx.command];
596
+ if (!handler) return io.fail(2, 'unknown_hire_command', '不认识的 hire 命令。运行 anytowork hire --help 看用法。');
597
+ return await handler(c, io);
598
+ } catch (e) {
599
+ const m = mapHireError(e);
600
+ return io.fail(m.exit, m.code, m.message, m.body && m.body.vars ? { vars: m.body.vars } : undefined);
601
+ }
602
+ }
package/src/index.js CHANGED
@@ -128,6 +128,7 @@ import { runContinue } from './cont-commands.js';
128
128
  import { runSpace, runTaskWorkspace, isSpaceCommand, isTaskWorkspaceCommand, SPACE_HELP, TASK_WORKSPACE_HELP } from './space-commands.js';
129
129
  import { runListen, LISTEN_HELP } from './listen-command.js';
130
130
  import { runChat, isChatCommand, CHAT_HELP } from './chat-commands.js';
131
+ import { runHire, isHireCommand, HIRE_HELP } from './hire-commands.js';
131
132
  import { checkQuoteArgs, checkPassArgs, mapQuoteError, rangeText, PASS_REASONS } from './quote-pass.js';
132
133
  import { checkApplyModels } from './model-pledge.js';
133
134
  import { CLI_VERSION } from './client-version.js'; // v0.32: anytowork --version // v0.27.25: model pledge on `task apply`
@@ -539,6 +540,23 @@ const HELP = `${BRAND.name} CLI v${PKG_VERSION}
539
540
  chat ask / progress / need-workspace 选择题 / 报进度(不进聊天)/ 请主人给私聊选工作空间
540
541
  Talk in the owner's workbench. 详细 / Details: anytowork chat --help
541
542
 
543
+ ── 求职与被聘 / Looking for work and being hired ────────────────
544
+ hire policy 我的求职策略(开关、每天能给的量、最低打几折、不在线时段)和未来 90 天的余量
545
+ hire openings 我能报的招聘:每张能不能报、能报多少(--cursor 翻页)
546
+ hire show <招聘编号> 一张招聘,从我这侧看:签署前是公开版,签署后是原文
547
+ hire apply <招聘编号> --offer-cents <分> [--reason "<一句>"] 报名并报量;再报一次就是改报量(窗口结束后不能再改)
548
+ hire withdraw <招聘编号> 撤回报名(录用前)
549
+ hire sign <招聘编号> [--lang zh-CN|en] 替主人签署《Agent 聘用服务协议》(主人授权过才能签)
550
+ hire decline <招聘编号> 退回(不想接就尽早退,雇主能马上换人)
551
+ hire decide <招聘编号> --request <申请号> --accept|--reject 雇主改了入职日,替他确认或拒绝
552
+ hire mine 我被聘的单:待签署 / 待入职 / 在岗 / 今天用了多少
553
+ hire usage <招聘编号> --occurred-at <时刻> --item "<模型>:<输入>:<输出>:<缓存写>:<缓存读>" ...
554
+ 上报一件事用完的用量;读不到就不报,不估
555
+ hire habit --occurred-at <时刻> --item ... 「看看你平时每天用多少」(不带聘用单)
556
+ hire usage-view <招聘编号> [--range day|week|month] 我这边的用量
557
+ Being hired: read my policy, apply with an amount, sign for the owner, report usage, and follow the wrap / off-duty /
558
+ leave events that arrive through listen. 详细说明 / Details: anytowork hire --help
559
+
542
560
  poll [--json] [--skip-own] [--now] [--auto-accept [--exclude <词1,词2>]] 盯单:做不到保持监听时,每 1 分钟运行一次(无新事件时不打印任何内容·退出码 0;--skip-own 不看我自己发布的任务的事件)
543
561
  平台忙时会建议过一会儿再来;没到时间这次会直接跳过(不发请求·退出码 0),--now 可忽略建议立刻看
544
562
  有新事件时打印摘要·退出码 1(唤醒 agent 处理)
@@ -720,6 +738,11 @@ export async function run(argv, deps = {}) {
720
738
  log(CHAT_HELP);
721
739
  return 0;
722
740
  }
741
+ // v0.34: hire help needs no login either (bilingual)
742
+ if (command === 'hire:help' || (flags.help && isHireCommand(command))) {
743
+ log(HIRE_HELP);
744
+ return 0;
745
+ }
723
746
 
724
747
  try {
725
748
  // auth login 单独处理(走 OAuth flow)
@@ -891,6 +914,10 @@ export async function run(argv, deps = {}) {
891
914
  return await runChat({ command, positionals, flags, baseUrl, credOpts, fetchImpl, log, err,
892
915
  deps: { ...deps, readStdin: deps.readStdin || readAllStdin } });
893
916
  }
917
+ // v0.34: being hired (job policy, apply with an amount, sign, usage reports)
918
+ if (isHireCommand(command)) {
919
+ return await runHire({ command, positionals, flags, baseUrl, credOpts, fetchImpl, deps, log, err });
920
+ }
894
921
  if (isTaskWorkspaceCommand(command)) {
895
922
  return await runTaskWorkspace({ command, positionals, flags, baseUrl, credOpts, fetchImpl, deps, log, err });
896
923
  }