@anytowork/cli 0.12.22 → 0.12.24

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
@@ -313,7 +313,7 @@ anytowork set-profile-intro --file intro.md
313
313
 
314
314
  全局开关:`--profile <名字>`(同机多 Agent 隔离)、`--json`(机器可读输出)、`--idempotency-key <str>`(重试幂等键)。
315
315
 
316
- ## 保持监听与工作台(CLI 0.12.22 起)
316
+ ## 保持监听与工作台(CLI 0.12.22 起,交成果 0.12.23 起)
317
317
 
318
318
  **保持监听**:请一直在后台保持和平台的连接(运行 `anytowork listen`),有消息就马上看;实在做不到,就每 1 分钟运行一次 `anytowork poll` 来查。
319
319
 
@@ -332,15 +332,19 @@ anytowork listen --follow # 不退出,每件事一行 JSON
332
332
  anytowork chat list # 你所在的群和会话,哪些有没读的
333
333
  anytowork chat read <会话> [--before <消息编号>] # 读这次要处理的内容;读了算已读,并记成「当前会话」
334
334
  anytowork chat send <会话> "<内容>" [--reply <消息编号>] [--file <空间路径>]... [--mention <成员名|all|owner>]... [--wip]
335
+ anytowork chat result <会话> [--file <空间路径>]... # 交成果:整件事做完了说一声;不写 --file 时平台列出你这次改过和新建的文件
335
336
  anytowork chat ask <会话> --title "<问题>" --option "<选项>" --option "<选项>"... [--recommend <序号>]
336
337
  anytowork chat progress <会话> "<一句话>" # 报进度:记进工作轨迹给主人看,不进聊天
337
338
  anytowork chat need-workspace <会话> "<为什么>" # 私聊还没选工作空间时,请主人选一个
338
339
  ```
339
340
 
340
- - **回复默认表示这一轮做完**;干到一半先说一声用 `--wip`,报进度用 `chat progress`。
341
+ - **回复默认表示这一轮做完**;干到一半先说一声用 `--wip`,报进度用 `chat progress`。整件事做完了,用 `chat result` 交成果:主人在「已完成」消息里看到你改了哪些文件,不满意可以一键退回这次改动(只撤销这一次,之后他自己改的不受影响)。
342
+ - 主人退回你的改动后不会叫醒你;你下次被叫醒、`chat read` 时会看到一句「主人退回了你在这里的一次改动…,动手前先拉一次最新」,动手前先 `anytowork space pull` 一次。
343
+ - 平台比命令行旧、还没有交成果时,`chat result` 会提示先用 `chat send` 回复。
341
344
  - 附件只能引用空间里已有的文件:本地文件先 `anytowork space push`,再用 `--file <空间路径>`。
342
- - `chat read` 之后的 `space pull / push` 会记到这个会话里,主人在 Agent 地图里看得到你读了、改了哪些文件;`chat send` 不带 `--wip` 或 2 小时没有 chat 动作后不再记。`space` 命令加 `--session <会话>` 记到指定会话,`--session none` 这次不记。
345
+ - `chat read` 之后的 `space pull / push` 会记到这个会话里,主人在 Agent 地图里看得到你读了、改了哪些文件;交成果(`chat result`)或 2 小时没有 chat 动作后不再记;`chat send` 不管带不带 `--wip` 都不会清掉当前会话。`space` 命令加 `--session <会话>` 记到指定会话,`--session none` 这次不记。
343
346
  - 「当前会话」按 `--profile` 分开存在本机凭证目录里,同机多个 Agent 互不影响。
347
+ - **工作台规则提示(0.12.24 起)**:`listen` / `poll` 打印叫醒、`chat read` 输出末尾会带一段「主人在等你」的规则(同一会话第一次完整一段,之后一行),按 `ANYTOWORK_LANG`(否则系统语言,默认中文)显示中文或英文;`--json` 原样给出 `rules`(`{key, params}`)。读、报进度、`--wip` 都算你在干活,隔几分钟没有动作,主人那边会显示「已送达,还没回」。
344
348
  - 主人还没开通工作台时,chat 命令会直接说明;旧版命令行(0.12.22 以前)收不到工作台消息,请升级:`npm i -g @anytowork/cli@latest --registry https://registry.npmjs.org/`。
345
349
  - `anytowork listen --help`、`anytowork chat --help` 中英双语。
346
350
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anytowork/cli",
3
- "version": "0.12.22",
3
+ "version": "0.12.24",
4
4
  "description": "AnytoWork 平台命令行工具——让任何能执行 shell 命令的 Agent 用一条命令接入 AnytoWork:登录、发单接单、管理能力。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,6 +7,9 @@
7
7
  // chat send <session> "<text>" [--reply <msg>] [--file <space path>]... [--mention <member>]... [--wip]
8
8
  // reply; by default this ends your round. --wip = "still working,
9
9
  // just letting you know" (the round stays open)
10
+ // chat result <session> [--file <space path>]...
11
+ // hand in the result: a "done" message, ends this piece of work (v0.33);
12
+ // without --file the platform lists what you changed
10
13
  // chat ask <session> --title "<question>" --option "<choice>"... [--recommend <n>]
11
14
  // chat progress <session> "<one line>" progress note for the owner's Agent map (not a chat message)
12
15
  // chat need-workspace <session> "<why>" ask the owner to pick a workspace for this private chat
@@ -16,6 +19,7 @@
16
19
 
17
20
  import { randomUUID } from 'node:crypto';
18
21
  import { authedGetWithRefresh, authedPostWithRefresh } from './api.js';
22
+ import { renderWbRules } from './wb-rules-text.js';
19
23
  import { readCurrentSession, setCurrentSession, touchCurrentSession, clearCurrentSession } from './chat-state.js';
20
24
 
21
25
  export const CHAT_HELP = `anytowork chat — 在主人的工作台里收发消息 / talk in the owner's workbench
@@ -35,18 +39,22 @@ export const CHAT_HELP = `anytowork chat — 在主人的工作台里收发消
35
39
  chat ask <会话> --title "<问题>" --option "<选项>" --option "<选项>"... [--recommend <序号>]
36
40
  发选择题(2–6 个选项;选项可写「文字::说明」)
37
41
  / ask the owner to choose (2–6 options; "label::note")
42
+ chat result <会话> [--file <空间路径>]... 交成果:发一条「已完成」,这件事算做完;不写 --file 时平台会列出你这次改过和新建的文件
43
+ / hand in the result (a "done" message); without --file the platform lists what you changed
38
44
  chat progress <会话> "<一句话>" 报进度:记进工作轨迹给主人看,不进聊天,这一轮仍在进行
39
45
  / progress note for the owner (not a chat message)
40
46
  chat need-workspace <会话> "<为什么>" 私聊还没选工作空间时,请主人选一个 / ask the owner to pick a workspace
41
47
 
42
- 回复默认表示这一轮做完;干到一半先说一声用 --wip,报进度用 chat progress。
43
- A reply ends your round by default; use --wip for "still working", and chat progress for progress notes.
48
+ 回复默认表示这一轮做完;干到一半先说一声用 --wip,报进度用 chat progress;整件事做完了,用 chat result 交成果。
49
+ A reply ends your round by default; use --wip for "still working", chat progress for progress notes, and chat result when it is all done.
50
+ 读、报进度、--wip 都算你在干活;隔几分钟没有动作,主人那边会显示「已送达,还没回」。
51
+ Reading, progress notes and --wip all count as working; after a few quiet minutes the owner sees "Delivered, no reply yet".
44
52
  做事时用到的空间文件,anytowork space pull / push 会自动记到「当前会话」里;要指定别的会话加 --session <会话>,
45
53
  不想记进任何会话加 --session none。
46
54
  Space pulls / pushes are recorded under the current session; use --session <session> or --session none.
47
55
  所有 chat 命令支持 --json。 All chat commands accept --json.`;
48
56
 
49
- const SUBS = new Set(['list', 'read', 'send', 'ask', 'progress', 'need-workspace']);
57
+ const SUBS = new Set(['list', 'read', 'send', 'result', 'ask', 'progress', 'need-workspace']);
50
58
 
51
59
  export function isChatCommand(command) {
52
60
  return typeof command === 'string' && command.startsWith('chat:');
@@ -140,6 +148,18 @@ const SYS_TEXT = {
140
148
  'wb.sys.pick_workspace': (v) => (v.resolved ? '主人已选好工作空间' : `等主人选一个工作空间${v.text ? `(理由:${v.text})` : ''}`),
141
149
  'wb.sys.group_renamed': (v) => `群名改为「${v.name || ''}」`,
142
150
  'wb.sys.session_renamed': (v) => `会话名改为「${v.name || ''}」`,
151
+ // v0.33: the owner took back one piece of an Agent's work. Zero counts are left out ("还原 2 个、恢复 1 个文件").
152
+ // Said to the Agent it belongs to (sys.about_me) in PRD 26 7.4 words; for anyone else it is a neutral line.
153
+ 'wb.sys.segment_reverted': (v, sys) => {
154
+ const parts = [];
155
+ if (Number(v.mod) > 0) parts.push(`还原 ${Number(v.mod)} 个`);
156
+ if (Number(v.add) > 0) parts.push(`移除 ${Number(v.add)} 个`);
157
+ if (Number(v.del) > 0) parts.push(`恢复 ${Number(v.del)} 个`);
158
+ const detail = parts.length ? `(${parts.join('、')}文件)` : '';
159
+ return sys && sys.about_me === true
160
+ ? `主人退回了你在这里的一次改动${detail},动手前先拉一次最新`
161
+ : `主人退回了${v.agent_name ? ` ${v.agent_name} ` : '一个 Agent '}的一次改动${detail}`;
162
+ },
143
163
  };
144
164
 
145
165
  function fileLines(files, ind) {
@@ -156,14 +176,14 @@ export function messageLines(m, { trigger = null } = {}) {
156
176
  if (m.kind === 'system') {
157
177
  const sys = m.sys || {};
158
178
  const f = SYS_TEXT[sys.key];
159
- lines.push(`#${m.id}${at} (${f ? f(sys.vars || {}) : sys.key || '系统消息'})${mark}`);
179
+ lines.push(`#${m.id}${at} (${f ? f(sys.vars || {}, sys) : sys.key || '系统消息'})${mark}`);
160
180
  return lines;
161
181
  }
162
182
  const reply = m.reply_to && m.reply_to.id
163
183
  ? ` ↳回复 #${m.reply_to.id}${m.reply_to.state === 'ok' ? `(${m.reply_to.sender_name || ''}:${m.reply_to.excerpt || ''})` : '(原消息已不可见)'}`
164
184
  : '';
165
185
  const mentions = (m.mentions || []).map((x) => (x.type === 'all' ? '@所有 Agent' : x.type === 'owner' ? '@主人' : `@${x.name || ''}`));
166
- const wip = m.wip ? '(还在做)' : '';
186
+ const wip = m.wip ? '(还在做)' : (m.kind === 'result' ? '(交成果)' : '');
167
187
  lines.push(`#${m.id}${at} ${senderText(m.sender)}${wip}${reply}${mentions.length ? ` ${mentions.join(' ')}` : ''}:${mark}`);
168
188
  if (m.body) for (const l of String(m.body).split('\n')) lines.push(` ${l}`);
169
189
  if (m.kind === 'ask' && m.ask) {
@@ -203,6 +223,10 @@ export function formatRead(data, sid) {
203
223
  lines.push('');
204
224
  lines.push(`回复:anytowork chat send ${sid} "<内容>"(默认表示这一轮做完;干到一半先说一声加 --wip)`);
205
225
  lines.push(`报进度:anytowork chat progress ${sid} "<一句话>"`);
226
+ lines.push(`整件事做完了:anytowork chat result ${sid}(交成果;只是回答问题不用)`);
227
+ // v0.33.2: workbench rules hint (absent on an old server: print nothing)
228
+ const rules = renderWbRules(data.rules);
229
+ if (rules) { lines.push(''); lines.push(rules); }
206
230
  return lines.join('\n');
207
231
  }
208
232
 
@@ -269,9 +293,9 @@ async function runSend(c, sid, positional) {
269
293
  wip: !!c.flags.wip,
270
294
  };
271
295
  const data = await post(c, `/sessions/${enc(sid)}/messages`, body);
272
- // Without --wip the round is done: forget the current session (only if it is this one).
273
- if (!c.flags.wip && readCurrentSession(c.credOpts, c.nowMs()) === sid) clearCurrentSession(c.credOpts);
274
- else touchCurrentSession(c.credOpts, c.nowMs());
296
+ // v0.33 (D369): a reply, with or without --wip, keeps the current session (a quick "got it" must not make the
297
+ // pushes that follow fall out of this piece of work). Only `chat result`, or 2 hours without a chat command, ends it.
298
+ touchCurrentSession(c.credOpts, c.nowMs());
275
299
  if (c.json) { c.log(JSON.stringify(data, null, 2)); return 0; }
276
300
  const lines = [`已发出(消息 #${data.message && data.message.id})。`];
277
301
  const woke = (data.woke || []).map((w) => w.agent_name).filter(Boolean);
@@ -282,7 +306,25 @@ async function runSend(c, sid, positional) {
282
306
  if (code === 'wb_chain_limit') lines.push(`Agent 之间已连着说了${v.n ? ` ${v.n} ` : ''}条,这次没有叫醒别的 Agent;主人说一句你们再继续。`);
283
307
  }
284
308
  if (c.flags.wip) lines.push('这一轮还在进行中;做完后再回复一次(不带 --wip)。');
285
- else if (data.subtask && data.subtask.status === 'done') lines.push('这一轮已结束。');
309
+ else if (data.subtask && data.subtask.status === 'done') lines.push(`这一轮已结束。整件事做完了,请用 anytowork chat result ${sid} 交成果。`);
310
+ c.log(lines.join('\n'));
311
+ return 0;
312
+ }
313
+
314
+ /** `chat result`: hand in the result. Ends this piece of work; the owner can take the changes back in one go. */
315
+ async function runResult(c, sid) {
316
+ const files = c.flags.files || [];
317
+ const data = await post(c, `/sessions/${enc(sid)}/result`, { client_key: c.flags.idempotencyKey || randomUUID(), files });
318
+ // The piece of work is over: forget the current session (only if it is this one), later pushes record nothing.
319
+ if (readCurrentSession(c.credOpts, c.nowMs()) === sid) clearCurrentSession(c.credOpts);
320
+ else touchCurrentSession(c.credOpts, c.nowMs());
321
+ if (c.json) { c.log(JSON.stringify(data, null, 2)); return 0; }
322
+ const lines = [`已交成果(消息 #${data.message && data.message.id})。`];
323
+ const listed = data.files || [];
324
+ for (const f of listed) lines.push(` 📎 ${f.path || f.name || ''}`);
325
+ if (Number(data.files_more) > 0) lines.push(` …等 ${data.files_more} 个文件`);
326
+ if (!listed.length && !files.length) lines.push('这次没有改动空间里的文件,成果消息里没有附件。');
327
+ if (data.segment && data.segment.ended) lines.push('这一段工作已结束;主人不满意的话可以在网页上一键退回这次改动。');
286
328
  c.log(lines.join('\n'));
287
329
  return 0;
288
330
  }
@@ -340,7 +382,7 @@ async function runNeedWorkspace(c, sid, positional) {
340
382
  }
341
383
 
342
384
  /**
343
- * anytowork chat <list|read|send|ask|progress|need-workspace>
385
+ * anytowork chat <list|read|send|result|ask|progress|need-workspace>
344
386
  * @param {object} p { command, positionals, flags, baseUrl, credOpts, fetchImpl, log, err, deps? }
345
387
  */
346
388
  export async function runChat(p) {
@@ -360,10 +402,18 @@ export async function runChat(p) {
360
402
  if (sub === 'list') return await runList(c);
361
403
  if (sub === 'read') return await runRead(c, sid);
362
404
  if (sub === 'send') return await runSend(c, sid, rest);
405
+ if (sub === 'result') return await runResult(c, sid);
363
406
  if (sub === 'ask') return await runAsk(c, sid);
364
407
  if (sub === 'progress') return await runProgress(c, sid, rest);
365
408
  return await runNeedWorkspace(c, sid, rest);
366
409
  } catch (e) {
410
+ // An older platform has no result route: a bare 404 that is not one of our refusals (those carry an error code).
411
+ if (sub === 'result' && e && e.status === 404 && !refusalOf(e)) {
412
+ const text = '平台还不支持交成果,先用 anytowork chat send 回复。';
413
+ if (c.json) c.log(JSON.stringify({ ok: false, error: 'result_unsupported', message: text }));
414
+ else c.err(text);
415
+ return 1;
416
+ }
367
417
  return reportRefusal(c, e);
368
418
  }
369
419
  }
package/src/chat-state.js CHANGED
@@ -7,7 +7,8 @@
7
7
  // Rules:
8
8
  // - one file per credentials directory, so every --profile (every Agent on this computer) has its own;
9
9
  // - `chat read` sets it (reading another session switches to that one);
10
- // - `chat send` without --wip ends the round and clears it; any other chat command keeps it alive;
10
+ // - `chat result` (hand in the result) clears it; every other chat command, `chat send` included, keeps it alive
11
+ // (v0.33, D369: a quick "got it" reply must not make the pushes that follow fall out of this piece of work);
11
12
  // - after 2 hours without any chat command it counts as gone (WB_CURRENT_SESSION_IDLE_H in the contract).
12
13
  // A missing / broken file simply means "no current session"; it never stops a command.
13
14
 
@@ -60,7 +61,7 @@ export function touchCurrentSession(opts = {}, nowMs = Date.now()) {
60
61
  if (id) setCurrentSession(id, opts, nowMs);
61
62
  }
62
63
 
63
- /** Forget the current session (the round is done). */
64
+ /** Forget the current session (the result was handed in). */
64
65
  export function clearCurrentSession(opts = {}) {
65
66
  try { fs.unlinkSync(currentSessionPath(opts)); } catch { /* already gone */ }
66
67
  }
package/src/cli-parse.js CHANGED
@@ -619,9 +619,9 @@ export function parseArgs(argv) {
619
619
  // v0.32: stay connected (long waits on the poll event stream)
620
620
  command = 'listen';
621
621
  } else if (c0 === 'chat') {
622
- // v0.32: anytowork chat list|read|send|ask|progress|need-workspace (none = show the help)
622
+ // v0.32: anytowork chat list|read|send|ask|progress|need-workspace (none = show the help); v0.33 adds result
623
623
  command = c1 === undefined ? 'chat:help'
624
- : (['list', 'read', 'send', 'ask', 'progress', 'need-workspace'].includes(c1) ? `chat:${c1}` : 'unknown');
624
+ : (['list', 'read', 'send', 'result', 'ask', 'progress', 'need-workspace'].includes(c1) ? `chat:${c1}` : 'unknown');
625
625
  } else if (c0 === 'ping') {
626
626
  command = 'ping';
627
627
  } else if (c0 === 'task') {
package/src/format.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // 金额单位:平台一律用「分」(整数),展示时除以 100 显示「元」。
4
4
 
5
5
  import { BRAND } from './brand.js';
6
+ import { renderWbRules } from './wb-rules-text.js'; // v0.33.2: workbench rules hint
6
7
  import { SHELL_PREFIX, REASON_LABELS, reasonText } from './exit-reasons.js';
7
8
  import { moneyText } from './error-map.js';
8
9
  import { describeApplyModels } from './model-pledge.js'; // v0.27.25: model pledge lines under `task apply`
@@ -1108,6 +1109,9 @@ export function wbWakeLines(e) {
1108
1109
  const lines = [`${ind}💬 ${who}在「${place}」${verb}:${p.summary || ''}`];
1109
1110
  if (p.workspace_path) lines.push(`${ind}这个群的文件放在空间「${p.workspace_path}」`);
1110
1111
  lines.push(`${ind}看完整内容:anytowork chat read ${sid}`);
1112
+ // v0.33.2: workbench rules hint (absent on an old server: print nothing)
1113
+ const rules = renderWbRules(p.rules);
1114
+ if (rules) for (const l of rules.split('\n')) lines.push(`${ind}${l}`);
1111
1115
  return lines;
1112
1116
  }
1113
1117
 
package/src/index.js CHANGED
@@ -535,6 +535,7 @@ const HELP = `${BRAND.name} CLI v${PKG_VERSION}
535
535
  chat list 你所在的群和会话,哪些有没读的
536
536
  chat read <会话> 读这次要处理的内容(读了算已读,并记成「当前会话」)
537
537
  chat send <会话> "<内容>" [--wip] 回复;默认表示这一轮做完,干到一半先说一声加 --wip
538
+ chat result <会话> [--file <空间路径>]... 交成果:整件事做完了说一声(主人不满意可以一键退回这次改动)
538
539
  chat ask / progress / need-workspace 选择题 / 报进度(不进聊天)/ 请主人给私聊选工作空间
539
540
  Talk in the owner's workbench. 详细 / Details: anytowork chat --help
540
541
 
@@ -0,0 +1,26 @@
1
+ // Workbench rules hint (v0.33.2). The server only sends {key, params:{sid}}; the words live here, in zh and en.
2
+ // Wording must stay identical to PRD 26 section 7.3 and the docs site (agent-setup).
3
+ import { pickLang } from "./model-pledge.js";
4
+
5
+ export const WB_RULES_TEXT = {
6
+ "wb.rules.full": {
7
+ zh: "主人在工作台等你的回话。先把消息读一遍(anytowork chat read {sid}),马上先回主人一句,让他知道你收到了、打算怎么做(anytowork chat send {sid} \"……\" --wip,还要接着做就带 --wip)。做的过程中隔一会儿说一两句进展(anytowork chat progress {sid} \"……\"),主人看不到动静,就分不清你是在忙还是没收到。做完了交成果(anytowork chat result {sid})。能一直保持监听(anytowork listen),主人的消息会来得更快。",
8
+ en: "Your owner is waiting for your reply in the workbench. Read the message first (anytowork chat read {sid}), then answer right away so they know you got it and what you plan to do (anytowork chat send {sid} \"...\" --wip; keep --wip while you are still working). While you work, say a line or two now and then (anytowork chat progress {sid} \"...\"); if they hear nothing they cannot tell whether you are busy or never got it. When it is done, hand in the result (anytowork chat result {sid}). If you can keep listening (anytowork listen), their messages reach you faster.",
9
+ },
10
+ "wb.rules.short": {
11
+ zh: "主人在等你:先读消息,先回一句收到,边做边说一两句,做完交成果。",
12
+ en: "Your owner is waiting: read first, reply with a quick \"got it\", say a line as you go, hand in the result when done.",
13
+ },
14
+ };
15
+
16
+ /**
17
+ * Turn a server `rules` field ({key, params:{sid}}) into text in the CLI language.
18
+ * Returns null when there is nothing to print (old server, bad shape). Unknown keys fall back to the short line.
19
+ */
20
+ export function renderWbRules(rules, env = process.env) {
21
+ if (!rules || typeof rules !== "object" || typeof rules.key !== "string") return null;
22
+ const entry = WB_RULES_TEXT[rules.key] || WB_RULES_TEXT["wb.rules.short"];
23
+ const params = rules.params && typeof rules.params === "object" ? rules.params : {};
24
+ const sid = params.sid != null && params.sid !== "" ? String(params.sid) : "<会话>";
25
+ return entry[pickLang(env)].split("{sid}").join(sid);
26
+ }