From 1fcf740495ca5ed585de966a90d9d2ac00a728e1 Mon Sep 17 00:00:00 2001 From: AutoPasha Date: Thu, 17 Sep 2026 14:52:58 +0000 Subject: [PATCH] =?UTF-8?q?0.2.0:=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B2=D0=BE=D0=BF=D1=80=D0=BE=D1=81=20=D1=81=20?= =?UTF-8?q?=D0=B2=D0=B0=D1=80=D0=B8=D0=B0=D0=BD=D1=82=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `answer --option "надпись"` передаёт выбранный вариант, а `status` печатает варианты строками под вопросом и говорит, когда вопрос ждёт значения в `--comment`. До этого вопрос с вариантами было не ответить из командной строки вовсе: сервер требует вариант из списка, а передать его было нечем, и увидеть список тоже негде. Заодно поправлен пример в README: поле вопроса называется question, а не text. --- CHANGELOG.md | 14 ++++++++++++ README.md | 14 ++++++++++-- package.json | 2 +- skill/SKILL.md | 11 ++++++++++ src/cli.mjs | 13 ++++++++--- src/render.mjs | 11 ++++++++++ test/cli.test.mjs | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 114 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b190416..d38449a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ ## [Не выпущено] +## [0.2.0] - 2026-09-17 + +### Добавлено + +- `answer --option "вариант"`: на вопрос с готовыми вариантами теперь можно + ответить. Раньше такой вопрос не отвечался вовсе — сервер требовал вариант из + списка, а передать его было нечем. +- `status` печатает варианты ответа строками под вопросом и говорит, когда + вопрос ждёт значения в `--comment`. + +### Исправлено + +- README: в примере с `jq` поле вопроса называется `question`, а не `text`. + ## [0.1.1] - 2026-09-12 ### Изменено diff --git a/README.md b/README.md index 234c854..63391fa 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ npx @autopasha/cli skill > .claude/skills/autopasha/SKILL.md | `autopasha status` | чем занят сотрудник, что стоит, какие вопросы открыты | | `autopasha feed [--since 2h]` | что произошло за срок | | `autopasha ask "текст" [--why "..."]` | поставить задачу | -| `autopasha answer да\|нет [--comment "..."]` | ответить на вопрос | +| `autopasha answer да\|нет [--comment "..."] [--option "вариант"]` | ответить на вопрос | | `autopasha say "текст"` | реплика в разговор, без задачи | | `autopasha watch [--since 1h] [--interval 20]` | ждать событий и печатать построчно | | `autopasha skill` | напечатать умение для своего агента | @@ -125,10 +125,20 @@ autopasha answer 8f2ac1b0-2f5e-4a11-9a6b-6c0f5a1d2e34 да --comment "бери Ответ будит сотрудника сразу, как кнопка в кабинете. Комментарий важнее самого решения: по нему видно границы, а не только факт согласия. +Бывает, что сотрудник предложил к вопросу готовые варианты: в `status` они +видны строками под вопросом. Тогда вариант надо назвать, иначе ответ не примут: + +```bash +autopasha answer 8f2ac1b0-2f5e-4a11-9a6b-6c0f5a1d2e34 да --option "на общий ящик" +``` + +А если вопрос ждёт значения (кода из СМС, суммы, ссылки), оно едет в +`--comment`: под вопросом так и написано, что именно заполнить. + Разобрать ответ кодом: ```bash -autopasha status --json | jq '.questions[] | {id, text}' +autopasha status --json | jq '.questions[] | {id, question, options}' ``` Два ключа на одной машине, если сотрудников у вас двое: diff --git a/package.json b/package.json index 0025ebe..40d76f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@autopasha/cli", - "version": "0.1.1", + "version": "0.2.0", "description": "Командная строка АвтоПаши: чем занят AI-сотрудник, поставить ему задачу, ответить на его вопрос", "keywords": [ "autopasha", diff --git a/skill/SKILL.md b/skill/SKILL.md index c3d2abb..0feaa4b 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -51,6 +51,17 @@ autopasha answer 8f2ac1b0-... да --comment "бери бесплатный ва Комментарий важнее самого решения: по нему сотрудник поймёт границы, а не только факт согласия. Ответ будит его сразу, как кнопка в кабинете. +Если под вопросом в `status` перечислены варианты строками «— …», ответ без +варианта не примут: назовите его надписью как есть. + +```bash +autopasha answer 8f2ac1b0-... да --option "на общий ящик" +``` + +А когда под вопросом написано «нужно заполнить: …» - вопрос ждёт значения, и +оно едет в `--comment`. Это не разрешение, а сам код, сумма или ссылка, ради +которых вопрос и задан. + ```bash autopasha say "клиент передумал, каталог не трогай" ``` diff --git a/src/cli.mjs b/src/cli.mjs index bb94da2..2a8c04e 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -20,7 +20,7 @@ const HELP = `autopasha — командная строка АвтоПаши. autopasha status чем занят сотрудник, что стоит, что спрашивает autopasha feed [--since 2h] что произошло autopasha ask "текст" [--why "…"] поставить задачу - autopasha answer да|нет [--comment "…"] ответить на вопрос + autopasha answer да|нет [--comment "…"] [--option "вариант"] ответить на вопрос autopasha say "текст" реплика в разговор autopasha watch [--since 1h] ждать событий и печатать их построчно autopasha skill напечатать умение для своего агента @@ -184,12 +184,19 @@ async function cmdAnswer({ client, args, opts, out }) { const [id, word] = args; if (!id) throw new UsageError('нужен id вопроса: autopasha answer да'); const decision = parseDecision(word); - const res = await client.answer(id, { decision, comment: opts.comment }, opts['idempotency-key']); + // `--option` — надпись варианта, если сотрудник их предложил. Смысл выбора + // («да» это или «нет») считает сервер по своей записи вопроса, мы передаём + // только надпись: вопрос про прод не должен переписываться из терминала. + const body = { decision }; + if (opts.comment) body.comment = opts.comment; + if (opts.option) body.option = opts.option; + const res = await client.answer(id, body, opts['idempotency-key']); if (opts.json) { out(JSON.stringify(res, null, 2)); return 0; } - out(`Ответ записан: ${decision === 'approved' ? 'да' : 'нет'}${opts.comment ? ` — ${opts.comment}` : ''}`); + const said = [opts.option, opts.comment].filter(Boolean).join(' — '); + out(`Ответ записан: ${decision === 'approved' ? 'да' : 'нет'}${said ? ` — ${said}` : ''}`); out('Сотрудник разбужен и вернётся к этому делу.'); return 0; } diff --git a/src/render.mjs b/src/render.mjs index e8e4ba8..517568a 100644 --- a/src/render.mjs +++ b/src/render.mjs @@ -40,6 +40,14 @@ export function renderStatus(s, now = new Date()) { for (const q of s.questions) { out.push(` ${q.id}`); out.push(` ${q.question}`); + // Варианты и поле печатаем всегда, когда они есть: без них ответ на такой + // вопрос отбивается сервером, а догадаться, чего он хочет, не по чему. + for (const o of q.options ?? []) { + out.push(` — ${o.label}${o.hint ? ` (${o.hint})` : ''}`); + } + if (q.input) { + out.push(` нужно ${q.input.required ? 'заполнить' : 'значение'}: ${q.input.label} → --comment`); + } const tail = []; if (q.ifSilent) tail.push(`молчание значит «${q.ifSilent}»`); else tail.push('без ответа встанет'); @@ -47,6 +55,9 @@ export function renderStatus(s, now = new Date()) { out.push(` ${tail.join(', ')}`); } out.push('', 'Ответить: autopasha answer да --comment "…"'); + if (s.questions.some((q) => q.options?.length)) { + out.push('Вопрос с вариантами — вариант надо назвать: autopasha answer да --option "надпись"'); + } } if (s.recent?.length) { diff --git a/test/cli.test.mjs b/test/cli.test.mjs index 7795fc5..d946e89 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -75,6 +75,61 @@ test('status показывает состояние, вопросы и как assert.match(text, /autopasha answer да/); }); +test('status показывает варианты ответа и поле, без которых вопрос не ответить', async () => { + const env = await sandbox(); + const out = collect(); + const code = await run(['status'], { + env: { ...env, AUTOPASHA_TOKEN: 'ap_live_test' }, + out: out.write, + err: out.write, + client: { + status: async () => ({ + employee: { name: 'Паша', state: 'работает' }, + waiting: { onOwner: 2, selfResolving: 0, total: 2 }, + questions: [ + { + id: 'q-1', + question: 'Куда писать?', + options: [ + { label: 'на общий ящик', decision: 'approved', hint: 'быстрее' }, + { label: 'искать личные', decision: 'approved' }, + ], + }, + { id: 'q-2', question: 'Код из СМС', input: { label: 'код из СМС', kind: 'number', required: true } }, + ], + recent: [], + }), + }, + }); + assert.equal(code, 0); + const text = out.text(); + assert.match(text, /— на общий ящик \(быстрее\)/); + assert.match(text, /— искать личные/); + assert.match(text, /нужно заполнить: код из СМС/); + // Подсказка про вариант появляется только тогда, когда вариант где-то есть. + assert.match(text, /--option "надпись"/); +}); + +test('answer передаёт выбранный вариант, а решение по нему считает сервер', async () => { + const env = await sandbox(); + const out = collect(); + let got; + const code = await run(['answer', 'q-1', 'да', '--option', 'на общий ящик'], { + env: { ...env, AUTOPASHA_TOKEN: 'ap_live_test' }, + out: out.write, + err: out.write, + client: { + answer: async (id, body) => { + got = { id, body }; + return { id, decision: body.decision }; + }, + }, + }); + assert.equal(code, 0); + assert.deepEqual(got, { id: 'q-1', body: { decision: 'approved', option: 'на общий ящик' } }); + assert.match(out.text(), /Ответ записан: да — на общий ящик/); +}); + test('feed --since 2h отсекает старое на нашей стороне, курсор не выдумывает', async () => { const env = await sandbox(); const out = collect();