herdrchat 1.2.1 → 1.3.1
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 +87 -23
- package/README.zh.md +70 -16
- package/bin/cli.mjs +126 -47
- package/bin/herdrchat.mjs +2 -2
- package/config.env.example +37 -20
- package/docs/architecture.md +13 -10
- package/docs/guide.md +134 -45
- package/lib/chats/contract.mjs +3 -2
- package/lib/chats/feishu/client.mjs +14 -5
- package/lib/chats/feishu/index.mjs +15 -2
- package/lib/chats/telegram/index.mjs +29 -7
- package/lib/core/config.mjs +28 -10
- package/lib/core/engine.mjs +17 -11
- package/lib/core/hc.mjs +57 -1
- package/lib/core/log.mjs +10 -4
- package/lib/core/readers/index.mjs +1 -1
- package/lib/core/service.mjs +1 -0
- package/package.json +1 -1
- package/skills/herdrchat/SKILL.md +9 -8
- package/skills/herdrchat/references/feishu.md +25 -3
- package/skills/herdrchat/references/ops.md +22 -52
- package/skills/herdrchat/references/telegram.md +31 -36
package/lib/core/engine.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// post(room, markdown) -> message; reply(where, markdown)
|
|
18
18
|
// postCard(room, look) -> message; editCard(message, look) look: {title, tone, markdown}
|
|
19
19
|
// postDialog(room, dialog, value) -> message; closeDialog(message, dialog, done)
|
|
20
|
-
// pin(k, room); unpin(k); pinned() -> keys; ring(k, message, text)
|
|
20
|
+
// pin(k, room, message); unpin(k); pinned() -> keys; ring(k, message, text)
|
|
21
21
|
// markWorking(message) -> token; markDone(message, token)
|
|
22
22
|
// image(buffer) -> markdown; download(message, ref, file)
|
|
23
23
|
// postFile(room, {name, data, caption}); postFolded(room, {title, markdown}); foldedBytes
|
|
@@ -58,7 +58,8 @@ const ENDED_REPLY = 'Not delivered: this terminal has ended. Nothing you send he
|
|
|
58
58
|
const WAIT = Symbol('wait');
|
|
59
59
|
const FILE_BYTES = 20 * 1000 * 1000;
|
|
60
60
|
|
|
61
|
-
// hc: {followEvents, cores, machines, getTerminal, screenTail, send, keys, put, run, kill
|
|
61
|
+
// hc: {followEvents, cores, machines, getTerminal, screenTail, send, keys, put, run, kill,
|
|
62
|
+
// nameTab?(tabId, name): name a tab in herdr-app, when it runs here};
|
|
62
63
|
// readers: {agentOf, readerFor}; store: {read, write} of state.json; inbox: a folder for
|
|
63
64
|
// images on their way to a terminal; quick: shorter waits (tests).
|
|
64
65
|
export function createEngine({ cfg, chat, hc, readers, store, log, logWarn, logError, startedAt = new Date().toISOString(), inbox = path.join(os.tmpdir(), 'herdrchat-inbox'), quick = false }) {
|
|
@@ -623,7 +624,7 @@ export function createEngine({ cfg, chat, hc, readers, store, log, logWarn, logE
|
|
|
623
624
|
if (!dialog.menu) {
|
|
624
625
|
const sent = await postTo(session, record, `**${dialog.title}**\n\n${dialog.markdown}\n\nReply \`/answer <text>\` to answer it, or \`/esc\` to cancel it.`);
|
|
625
626
|
const room = read().chats[k]?.chat;
|
|
626
|
-
if (room) await chat.pin(k, room);
|
|
627
|
+
if (room) await chat.pin(k, room, sent);
|
|
627
628
|
await chat.ring(k, sent, '⚠️ needs you');
|
|
628
629
|
} else {
|
|
629
630
|
const message = await deliver(session, record, (room) => chat.postDialog(room, dialog, dialogValue(session, address, dialog)));
|
|
@@ -631,7 +632,7 @@ export function createEngine({ cfg, chat, hc, readers, store, log, logWarn, logE
|
|
|
631
632
|
remember(message, dialog);
|
|
632
633
|
openCard.set(k, { message, dialog });
|
|
633
634
|
const room = read().chats[k]?.chat;
|
|
634
|
-
if (room) await chat.pin(k, room);
|
|
635
|
+
if (room) await chat.pin(k, room, message);
|
|
635
636
|
await chat.ring(k, message, '⚠️ needs you');
|
|
636
637
|
// answered while the card was on its way: nothing will close it later, so now
|
|
637
638
|
const live = views.get(session)?.terminals.get(address);
|
|
@@ -952,8 +953,9 @@ export function createEngine({ cfg, chat, hc, readers, store, log, logWarn, logE
|
|
|
952
953
|
log(`-> ${session}/${address}: ${text.slice(0, 80)}`);
|
|
953
954
|
await markWorking(k, message);
|
|
954
955
|
} catch (err) {
|
|
955
|
-
|
|
956
|
-
|
|
956
|
+
// the screen only when it helps: it waits on something you can answer from there
|
|
957
|
+
const waits = /blocked/i.test(err.message);
|
|
958
|
+
await chat.reply(where, `Not delivered: ${plainError(err)}${waits ? `\n\n${await screen(session, address)}\n\nIt waits on a dialog. Reply \`/answer <text>\` to answer it, or \`/esc\` to cancel it.` : ''}`);
|
|
957
959
|
return;
|
|
958
960
|
}
|
|
959
961
|
// Anything but an agent posts nothing by itself: what you typed gets one look at its
|
|
@@ -1028,7 +1030,7 @@ export function createEngine({ cfg, chat, hc, readers, store, log, logWarn, logE
|
|
|
1028
1030
|
'`/new <session> <space>/<tab> [command]` start a terminal (default `claude`); `--cwd <dir>` first to pick its folder',
|
|
1029
1031
|
'`/kill <session> <address>` end a terminal',
|
|
1030
1032
|
...(chat.help || []),
|
|
1031
|
-
|
|
1033
|
+
`In a terminal's ${chat.roomWord}: type to talk to it; \`/screen\`, \`/answer <text>\`, \`/esc\`, \`/history\` (the conversation so far, as a file).`,
|
|
1032
1034
|
].join('\n');
|
|
1033
1035
|
|
|
1034
1036
|
async function newTerminal(args) {
|
|
@@ -1047,13 +1049,17 @@ export function createEngine({ cfg, chat, hc, readers, store, log, logWarn, logE
|
|
|
1047
1049
|
const spaceId = sibling?.labels['herdr.space'] || `sp${crypto.randomBytes(16).toString('hex')}`;
|
|
1048
1050
|
const folder = cwd || sibling?.foreground?.cwd || sibling?.cwd || os.homedir();
|
|
1049
1051
|
const line = command.length ? command.join(' ') : 'claude';
|
|
1052
|
+
const tabId = `tb${crypto.randomBytes(16).toString('hex')}`;
|
|
1050
1053
|
const out = await hc.run(session, {
|
|
1051
1054
|
cwd: folder,
|
|
1052
|
-
labels: { 'herdr.space': spaceId, 'herdr.tab':
|
|
1055
|
+
labels: { 'herdr.space': spaceId, 'herdr.tab': tabId, where: `${space} / ${tab}` },
|
|
1053
1056
|
argv: ['/bin/zsh', '-lc', `exec ${line}`], // login shell for PATH; exec so no bare shell is left behind
|
|
1054
1057
|
});
|
|
1055
1058
|
const address = JSON.parse(out).address;
|
|
1056
|
-
|
|
1059
|
+
// herdr-app makes the tab and numbers it, whatever `where` says: it is named as asked,
|
|
1060
|
+
// by the tab id it keeps (nothing happens where herdr-app is not running)
|
|
1061
|
+
if (hc.nameTab) hc.nameTab(tabId, tab).catch((err) => logWarn(`tab ${tab} not named in herdr-app: ${err.message}`));
|
|
1062
|
+
return `Started \`${line}\` as \`${address}\` in ${session} / ${space} / ${tab} (\`${folder}\`). Its ${chat.roomWord} appears in a moment.`;
|
|
1057
1063
|
}
|
|
1058
1064
|
|
|
1059
1065
|
async function direct(where, text) {
|
|
@@ -1073,14 +1079,14 @@ export function createEngine({ cfg, chat, hc, readers, store, log, logWarn, logE
|
|
|
1073
1079
|
async function onMessage({ room, where, message, text, images = [] }) {
|
|
1074
1080
|
try {
|
|
1075
1081
|
if (!room) {
|
|
1076
|
-
if (images.length) await chat.reply(where,
|
|
1082
|
+
if (images.length) await chat.reply(where, `Send images in a terminal's ${chat.roomWord}.`);
|
|
1077
1083
|
else await direct(where, text);
|
|
1078
1084
|
return;
|
|
1079
1085
|
}
|
|
1080
1086
|
const target = terminalOfRoom(room);
|
|
1081
1087
|
if (!target) {
|
|
1082
1088
|
log(`message in a room this bot did not make: ${room}`);
|
|
1083
|
-
await chat.reply(where,
|
|
1089
|
+
await chat.reply(where, `Not delivered: this ${chat.roomWord} is not a terminal's. Each terminal has its own ${chat.roomWord}; the direct chat takes /ls, /new and /kill.`);
|
|
1084
1090
|
return;
|
|
1085
1091
|
}
|
|
1086
1092
|
if (views.get(target.session)?.terminals.get(target.address)?.state !== 'live') {
|
package/lib/core/hc.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// Thin wrapper over the hc CLI. Every call names its session (HC_SESSION),
|
|
2
2
|
// never the default core.
|
|
3
3
|
import { execFile } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
4
8
|
|
|
5
9
|
// The bot runs in an hc terminal itself. Called with that terminal's identity, hc would
|
|
6
10
|
// wrap what the bot types in <hc from="tN"> tags, as a note from another agent; without
|
|
@@ -68,7 +72,8 @@ export function run(session, { name, cwd, labels = {}, argv }) {
|
|
|
68
72
|
|
|
69
73
|
// force: type even when the program is blocked on a dialog (it answers the dialog).
|
|
70
74
|
export function send(session, address, text, { force = false } = {}) {
|
|
71
|
-
|
|
75
|
+
// `--`: what you typed is never taken for an option, even when it starts with "-"
|
|
76
|
+
const args = ['send', ...(force ? ['--force'] : []), address, '--', text];
|
|
72
77
|
return hc(session, args, { timeoutMs: 60000 });
|
|
73
78
|
}
|
|
74
79
|
|
|
@@ -96,6 +101,57 @@ export function isUnavailable(err) {
|
|
|
96
101
|
return /"kind":"unavailable"/.test(err?.message || '');
|
|
97
102
|
}
|
|
98
103
|
|
|
104
|
+
// The hc sessions on this machine, from hc's own folder (asking hc about a session that
|
|
105
|
+
// does not exist would start one: a typo must not make a session).
|
|
106
|
+
export const hcStateRoot = (env = process.env) => path.join(env.XDG_STATE_HOME || path.join(os.homedir(), '.local', 'state'), 'herdr-core');
|
|
107
|
+
export function sessionsHere() {
|
|
108
|
+
try {
|
|
109
|
+
return fs.readdirSync(hcStateRoot(), { withFileTypes: true }).filter((d) => d.isDirectory() && fs.existsSync(path.join(hcStateRoot(), d.name, 'core.json'))).map((d) => d.name).sort();
|
|
110
|
+
} catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Whether a session's hc core runs, without starting it: its socket (named in core.lock)
|
|
116
|
+
// takes a connection.
|
|
117
|
+
export function coreUp(session, timeoutMs = 1000) {
|
|
118
|
+
let socket;
|
|
119
|
+
try {
|
|
120
|
+
socket = fs.readFileSync(path.join(hcStateRoot(), session, 'core.lock'), 'utf8').trim().split('\n')[0];
|
|
121
|
+
} catch {
|
|
122
|
+
return Promise.resolve(false);
|
|
123
|
+
}
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
const c = net.connect(socket);
|
|
126
|
+
const done = (up) => { c.destroy(); resolve(up); };
|
|
127
|
+
c.once('connect', () => done(true));
|
|
128
|
+
c.once('error', () => done(false));
|
|
129
|
+
setTimeout(() => done(false), timeoutMs);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// herdr-app names the tab with this id (it may take a few seconds to show it), when
|
|
134
|
+
// herdr-app runs on this machine; resolves false when it is not there or never shows it.
|
|
135
|
+
const TAB_TRIES = 10;
|
|
136
|
+
export async function nameTab(tabId, name, { tries = TAB_TRIES, waitMs = 1000 } = {}) {
|
|
137
|
+
const app = (args) => new Promise((resolve, reject) => execFile('herdr-app', args, { env: ENV, timeout: 10000 }, (err, stdout) => (err ? reject(err) : resolve(stdout))));
|
|
138
|
+
for (let i = 0; i < tries; i += 1) {
|
|
139
|
+
let spaces;
|
|
140
|
+
try {
|
|
141
|
+
spaces = JSON.parse(await app(['ls'])).spaces || [];
|
|
142
|
+
} catch {
|
|
143
|
+
return false; // no herdr-app here, or no window
|
|
144
|
+
}
|
|
145
|
+
const found = spaces.flatMap((s) => s.tabs || []).find((t) => t.id === tabId || t.id.endsWith(`/${tabId}`));
|
|
146
|
+
if (found) {
|
|
147
|
+
if (found.name !== name) await app(['do', 'tab.rename', '--id', found.id, '--name', name]);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
99
155
|
// A failed hc call as a person reads it: hc's own message, without the command line or
|
|
100
156
|
// its JSON wrapping.
|
|
101
157
|
export function plainError(err) {
|
package/lib/core/log.mjs
CHANGED
|
@@ -9,6 +9,9 @@ import path from 'node:path';
|
|
|
9
9
|
import util from 'node:util';
|
|
10
10
|
|
|
11
11
|
const KEEP_DAYS = 14;
|
|
12
|
+
|
|
13
|
+
// A log file per local day (what "today" means to you); the time on each line is UTC.
|
|
14
|
+
export const localDay = (d = new Date()) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
12
15
|
const CRASH_WAIT_MS = 3000;
|
|
13
16
|
|
|
14
17
|
export function createLog(dir) {
|
|
@@ -19,7 +22,7 @@ export function createLog(dir) {
|
|
|
19
22
|
const prune = (today) => {
|
|
20
23
|
if (pruned === today) return;
|
|
21
24
|
pruned = today;
|
|
22
|
-
const oldest = new Date(Date.now() - KEEP_DAYS * 86400 * 1000)
|
|
25
|
+
const oldest = localDay(new Date(Date.now() - KEEP_DAYS * 86400 * 1000));
|
|
23
26
|
for (const f of fs.readdirSync(dir)) {
|
|
24
27
|
if (/^\d{4}-\d{2}-\d{2}\.log$/.test(f) && f.slice(0, 10) < oldest) fs.rmSync(path.join(dir, f), { force: true });
|
|
25
28
|
}
|
|
@@ -32,8 +35,8 @@ export function createLog(dir) {
|
|
|
32
35
|
const line = `${now} ${level} ${text}`;
|
|
33
36
|
(level === 'ERROR' ? out.error : out.log)(line);
|
|
34
37
|
try {
|
|
35
|
-
fs.appendFileSync(path.join(dir, `${
|
|
36
|
-
prune(
|
|
38
|
+
fs.appendFileSync(path.join(dir, `${localDay()}.log`), `${line}\n`, { mode: 0o600 }); // it quotes your messages
|
|
39
|
+
prune(localDay());
|
|
37
40
|
} catch {
|
|
38
41
|
// the disk is the last resort; stdout has the line
|
|
39
42
|
}
|
|
@@ -46,7 +49,10 @@ export function createLog(dir) {
|
|
|
46
49
|
};
|
|
47
50
|
|
|
48
51
|
log.capture = ({ onCrash } = {}) => {
|
|
49
|
-
|
|
52
|
+
// a library that prints its errors as plain log lines ("[error]: ...", as the Feishu
|
|
53
|
+
// SDK does) gets them logged as WARN, so `herdrchat logs --errors` finds them
|
|
54
|
+
const levelOf = (args) => (/^\[(error|warn)\]/i.test(String(args[0] ?? '')) ? 'WARN' : 'INFO');
|
|
55
|
+
console.log = (...args) => write(levelOf(args), args);
|
|
50
56
|
console.info = console.log;
|
|
51
57
|
console.warn = (...args) => write('WARN', args);
|
|
52
58
|
console.error = (...args) => write('ERROR', args);
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// Cursors are in ../cursor.mjs.
|
|
17
17
|
//
|
|
18
18
|
// To add an agent: write lib/core/readers/<agent>.mjs and list it in LISTED below
|
|
19
|
-
// (docs/architecture.md, "Adding
|
|
19
|
+
// (docs/architecture.md, "Adding an agent").
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import * as claude from './claude.mjs';
|
|
22
22
|
|
package/lib/core/service.mjs
CHANGED
|
@@ -76,6 +76,7 @@ const run = (cmd, args) => execFileSync(cmd, args, { encoding: 'utf8', stdio: ['
|
|
|
76
76
|
|
|
77
77
|
// {kind, name, file, content, write, remove, installed, load, unload, restart, state,
|
|
78
78
|
// nodeGone}, or null on a system with neither launchd nor systemd.
|
|
79
|
+
// `profile` here is the profile's id (placesOf: the profile, tagged when its folder is moved).
|
|
79
80
|
export function serviceOf({ profile, root, node, env, stateDir, platform = process.platform, home = os.homedir() }) {
|
|
80
81
|
const script = path.join(root, 'bin', 'herdrchat.mjs');
|
|
81
82
|
const out = path.join(stateDir, 'logs', 'service.out.log');
|
package/package.json
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: herdrchat
|
|
3
|
-
description: Set up, run and look after herdrchat,
|
|
3
|
+
description: Set up, run and look after herdrchat, which mirrors every herdr-app (hc) tab into Feishu (a group per tab) or Telegram (a topic per tab): first setup of either app, start, stop, status, logs, running it as a service, settings, fixing problems, the Feishu developer console and @BotFather. Use for anything about herdrchat or its Feishu or Telegram bot, even "set up the Telegram bot", "is it running", "it does not post" or "mute the workers". Trigger with "/herdrchat", "/herdrchat setup", "/herdrchat status", "/herdrchat fix".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# herdrchat
|
|
7
7
|
|
|
8
8
|
herdrchat mirrors every herdr-app tab into a chat app: a group per tab in Feishu, a topic
|
|
9
9
|
per tab in the bot's private chat in Telegram; the conversation as it happens, dialogs as
|
|
10
|
-
buttons, an overview in the direct chat. One profile is one chat app (`HERDRCHAT_CHAT`)
|
|
11
|
-
|
|
10
|
+
buttons, an overview in the direct chat. One profile is one chat app (`HERDRCHAT_CHAT`):
|
|
11
|
+
one app uses the default profile; a second app beside it gets its own (`--profile <name>`).
|
|
12
12
|
|
|
13
13
|
Its command is `herdrchat` (`node bin/cli.mjs` in a clone): check with `command -v
|
|
14
14
|
herdrchat`; when it is missing, ask before `npm install -g herdrchat`. A profile
|
|
15
15
|
(`--profile`, default `default`) keeps its settings in
|
|
16
16
|
`~/.config/herdrchat/<profile>/config.env` and its state in
|
|
17
|
-
`~/.local/state/herdrchat/<profile>/`; `herdrchat status` prints both.
|
|
17
|
+
`~/.local/state/herdrchat/<profile>/`; `herdrchat status` prints both. Files named here
|
|
18
|
+
from the package (`config.env.example`, `docs/guide.md`) are in the installed package,
|
|
19
|
+
`$(npm root -g)/herdrchat/`, or in the clone.
|
|
18
20
|
|
|
19
21
|
## Commands
|
|
20
22
|
|
|
21
23
|
| Command | Do |
|
|
22
24
|
|---|---|
|
|
23
|
-
| `/herdrchat setup` | First ask which chat app (Feishu, Telegram, or both: one profile
|
|
25
|
+
| `/herdrchat setup` | First ask which chat app (Feishu, Telegram, or both: the second one on its own profile). Telegram: `/herdrchat telegram setup`. Feishu, in this order: create the app, the bot ability and the scopes (`references/feishu.md`); `herdrchat init` and fill `config.env` with the user (App ID, App Secret, `HERDR_SESSIONS`: `ls ~/.local/state/herdr-core` lists the sessions); `herdrchat start` (the console's long-connection check needs it running); events and callbacks; publish a version; the user messages the bot and adds the `FEISHU_OWNER_OPEN_ID` line it answers with; `herdrchat start` again; offer `/herdrchat auth` and `herdrchat install`. |
|
|
24
26
|
| `/herdrchat status` | `herdrchat status`; explain each line; on errors, `/herdrchat fix`. |
|
|
25
27
|
| `/herdrchat start`, `/herdrchat stop` | Read `config.env` (its `HERDR_SESSIONS`) in one step, then `herdrchat start` / `stop` in the next; never both at once. With a service installed they go through it. |
|
|
26
28
|
| `/herdrchat install`, `/herdrchat uninstall` | Run it as a service of the system (launchd on macOS, systemd on Linux): it starts at login and comes back after a crash, whatever happens to the hc cores. Recommended once it works. Read the config first; after a node upgrade, install again (`herdrchat status` says so). |
|
|
@@ -29,7 +31,7 @@ herdrchat`; when it is missing, ask before `npm install -g herdrchat`. A profile
|
|
|
29
31
|
| `/herdrchat config` | Change a setting in `config.env` with the user (sessions, `HERDR_MUTED_SPACES`, `HERDR_SILENT_SPACES`, `HERDR_BOT_TAGS`, `HERDR_USER_TAGS`, `HERDR_HISTORY`, `HERDRCHAT_CHAT`; each is explained in `config.env.example`), read the file back, then restart. |
|
|
30
32
|
| `/herdrchat fix` | Find the cause from `herdrchat status`, the logs and the live result, then fix it: `references/ops.md`. |
|
|
31
33
|
| `/herdrchat feishu <task>` | Anything in the Feishu developer console: scopes, callbacks, publishing, the icon, deleting an app: `references/feishu.md`. |
|
|
32
|
-
| `/herdrchat telegram <task>` | The Telegram bot
|
|
34
|
+
| `/herdrchat telegram <task>` | The Telegram bot (the default profile when it is the only app): `setup` (@BotFather, topic mode, token, owner id, first start), checking the token, quiet and silent spaces, what Telegram cannot do and what stands in: `references/telegram.md`. |
|
|
33
35
|
|
|
34
36
|
With no subcommand, run `herdrchat status` and suggest the next step from it.
|
|
35
37
|
|
|
@@ -39,8 +41,7 @@ With no subcommand, run `herdrchat status` and suggest the next step from it.
|
|
|
39
41
|
disbanding anything, sending to a real group. One yes covers one action.
|
|
40
42
|
- Never print, screenshot or paste `FEISHU_APP_SECRET`, `TELEGRAM_BOT_TOKEN` or
|
|
41
43
|
`user-token.json`; ask the user to paste a token into `config.env` themselves.
|
|
42
|
-
- A check that decides an action runs on its own first (read the config, then start
|
|
43
|
-
scan, then commit).
|
|
44
|
+
- A check that decides an action runs on its own first (read the config, then start).
|
|
44
45
|
- Say what was checked on the live bot and what was not.
|
|
45
46
|
|
|
46
47
|
## References
|
|
@@ -53,9 +53,10 @@ that applies or publishes something, and read it.
|
|
|
53
53
|
### Create the app (first setup only)
|
|
54
54
|
|
|
55
55
|
Developer console home -> create a custom app (企业自建应用) with a name, a description
|
|
56
|
-
and an icon -> Add capabilities -> Bot. Then import the scopes,
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
and an icon -> Add capabilities -> Bot. Then import the scopes, have the user fill
|
|
57
|
+
`config.env` (`herdrchat init`), start the bot (`herdrchat start`: the long-connection
|
|
58
|
+
check below needs it running), set events and callbacks, and publish. "Set up: Feishu"
|
|
59
|
+
in `docs/guide.md` lists the same steps from the user's side.
|
|
59
60
|
|
|
60
61
|
### Import scopes
|
|
61
62
|
|
|
@@ -107,6 +108,27 @@ Deactivate it first in the tenant admin console,
|
|
|
107
108
|
`https://<tenant>.feishu.cn/admin/appCenter/manage/<APP_ID>` (needs a tenant admin),
|
|
108
109
|
then delete it in the developer console. Ask before each of the two steps.
|
|
109
110
|
|
|
111
|
+
### The bot menu (optional)
|
|
112
|
+
|
|
113
|
+
A menu at the bottom of the direct chat with the bot, so the commands are a tap away as
|
|
114
|
+
Telegram's "/" list is. Feishu sets it only in the developer console (no API), shows it
|
|
115
|
+
in the direct chat only (not in groups), and needs a published version (then about five
|
|
116
|
+
minutes, and a client of 5.27 or later).
|
|
117
|
+
|
|
118
|
+
1. 应用能力 -> 机器人 -> 机器人自定义菜单: edit, turn it on (开启), pick a style.
|
|
119
|
+
2. Add items whose action is 推送事件 (push an event), with these keys; the bot treats a
|
|
120
|
+
click as that command in the direct chat:
|
|
121
|
+
| Item | Event key |
|
|
122
|
+
|---|---|
|
|
123
|
+
| Overview | `ls` |
|
|
124
|
+
| Help | `help` |
|
|
125
|
+
| Act as me | `auth` |
|
|
126
|
+
3. 事件与回调 -> 事件配置 (long connection mode, as for messages) -> 添加事件:
|
|
127
|
+
机器人自定义菜单事件 (`application.bot.menu_v6`).
|
|
128
|
+
4. Publish a version. Ask before publishing.
|
|
129
|
+
|
|
130
|
+
Commands that take words (`/new`, `/kill`) stay typed.
|
|
131
|
+
|
|
110
132
|
## Scopes this bot uses
|
|
111
133
|
|
|
112
134
|
Bot identity (the bot itself) and user identity (only for `/auth`, acting as the user):
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
# Ops: running and looking after herdrchat
|
|
2
2
|
|
|
3
|
-
The bot
|
|
4
|
-
hc session listed in `HERDR_SESSIONS`; `herdrchat start` (re)starts it and keeps it
|
|
5
|
-
running (`node bin/cli.mjs` from a clone). A profile (`--profile` or `HERDRCHAT_PROFILE`,
|
|
6
|
-
default `default`; another one runs as the hc terminal `herdrchat-<profile>`) has its
|
|
7
|
-
settings in `~/.config/herdrchat/<profile>/config.env` and writes to
|
|
8
|
-
`~/.local/state/herdrchat/<profile>/` (call it `D` below), unless `HERDRCHAT_HOME` holds
|
|
9
|
-
both (`config.env`, `state/`). Its settings in Feishu itself (scopes, callbacks, icon,
|
|
10
|
-
versions) are in `references/feishu.md`.
|
|
3
|
+
The bot (`bin/herdrchat.mjs` in the installed package) runs one of two ways:
|
|
11
4
|
|
|
12
|
-
|
|
5
|
+
- as a **service** of the system, after `herdrchat install` (launchd on macOS, systemd on
|
|
6
|
+
Linux): it starts at login and comes back after a crash; recommended;
|
|
7
|
+
- in an **hc terminal** named `herdrchat` (`herdrchat-<profile>` for another profile), in
|
|
8
|
+
the first session of `HERDR_SESSIONS`, after `herdrchat start` without a service.
|
|
9
|
+
|
|
10
|
+
`herdrchat status` says which, and where the profile's files are: settings in
|
|
11
|
+
`~/.config/herdrchat/<profile>/config.env`, state and logs in
|
|
12
|
+
`~/.local/state/herdrchat/<profile>/` (call it `D` below). The app's own settings (Feishu
|
|
13
|
+
scopes, callbacks; the Telegram bot's topic mode) are in `references/feishu.md` and
|
|
14
|
+
`references/telegram.md`.
|
|
13
15
|
|
|
14
16
|
## Is it running
|
|
15
17
|
|
|
16
18
|
```sh
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
herdrchat
|
|
20
|
-
herdrchat
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
herdrchat status # how it runs, sessions, keys set or missing, errors today
|
|
20
|
+
herdrchat logs --errors # what went wrong today ("[feishu] connected" / "[telegram] connected as @…" when in)
|
|
21
|
+
herdrchat start # restart: stops it everywhere, waits up to a minute until it is in
|
|
22
|
+
herdrchat install # make it a service: starts at login, back after a crash
|
|
23
|
+
launchctl print gui/$(id -u)/app.herdrchat.<profile> # macOS: the service's state, pid, last exit
|
|
24
|
+
systemctl --user status herdrchat-<profile> # Linux: the same
|
|
23
25
|
```
|
|
24
26
|
|
|
25
27
|
A restart is needed after any code or `config.env` change. It is safe: what was posted
|
|
@@ -28,7 +30,7 @@ is in `D/state.json`, so it picks up where it stopped.
|
|
|
28
30
|
While the bot is down the user hears nothing, so:
|
|
29
31
|
|
|
30
32
|
- Never end with the bot down: `herdrchat start` exits 1 with its errors when the bot
|
|
31
|
-
does not reach
|
|
33
|
+
does not reach its chat app within 60 s; fix that before anything else.
|
|
32
34
|
- Never stop or restart the hc core (`hc server stop|restart`) of the session the bot
|
|
33
35
|
runs in without saying so first, and run `herdrchat start` right after.
|
|
34
36
|
- Never start an hc core from an agent's shell: the core passes that shell's variables
|
|
@@ -73,49 +75,17 @@ grep -h ' <- \| -> ' D/logs/$(date -u +%F).log | tail # posts out, your messag
|
|
|
73
75
|
Read it with a short `node -e` and print only what you need. `D/user-token.json` is
|
|
74
76
|
your token after `/auth`: never print it.
|
|
75
77
|
|
|
76
|
-
## Did the change take effect
|
|
77
|
-
|
|
78
|
-
Check the live result, not only the code: count what actually happened in Feishu with
|
|
79
|
-
the bot's own client (`lib/chats/feishu/client.mjs` `createClient` with `loadConfig()`), for example:
|
|
80
|
-
|
|
81
|
-
- mentions in a group since a time: `im.v1.message.list` on the chat, then each item's
|
|
82
|
-
`mentions` (a mention inside a card is not counted by Feishu, so it does not ring);
|
|
83
|
-
- a group's bot tags: `im.v2.bizEntityTagRelation.get` with `tag_biz_type: 'chat'`;
|
|
84
|
-
- whether a card was edited: `im.v1.message.get`, compare `update_time` with `create_time`.
|
|
85
|
-
|
|
86
|
-
Say which checks were run and which were not; "the code is there" is not "it works".
|
|
87
|
-
|
|
88
78
|
## Usual problems
|
|
89
79
|
|
|
90
80
|
| You see | Likely cause | Do |
|
|
91
81
|
|---|---|---|
|
|
92
|
-
| nothing posts | bot down, or `FEISHU_OWNER_OPEN_ID`
|
|
82
|
+
| nothing posts | bot down, or the owner not set (`FEISHU_OWNER_OPEN_ID`, `TELEGRAM_OWNER_ID`) | `herdrchat status`; the log says "owner not set" |
|
|
83
|
+
| Telegram: no topic appears | topic mode off | log: "topic mode is off"; BotFather's mini app, the bot, Threaded Mode, then start again |
|
|
84
|
+
| Telegram: `telegram poll: ... 409` | another copy polls the same token | stop it (another machine?), then start once |
|
|
93
85
|
| a button gives error 200340 | the card callback is not set up | `references/feishu.md`: events and card callbacks |
|
|
94
86
|
| a dialog had no card | answered in the terminal before the card; or no numbered menu on screen | log: `blocked`; the card says `/answer` when it saw no menu |
|
|
95
87
|
| the phone rings too much | Feishu: not `/auth`ed; either app: settings | Feishu `/auth`; `HERDR_MUTED_SPACES` (quiet but for needs you and done), `HERDR_SILENT_SPACES` (nothing rings), per profile |
|
|
96
88
|
| a ⚠️ warning in the direct chat about acting as you | the token could not be refreshed | send `/auth` again |
|
|
97
89
|
| a group keeps an old name | its terminal is not live | it renames when the terminal is back |
|
|
98
90
|
| a Feishu call fails with 400 in a burst | a rate limit (tags: 5 a second) | space the calls; the bot queues tag calls |
|
|
99
|
-
|
|
100
|
-
## Trying a Feishu API safely
|
|
101
|
-
|
|
102
|
-
1. Read the official doc first (open.feishu.cn); write down the scope and limits.
|
|
103
|
-
2. Make real calls only with this repo's own code (the SDK or `fetch`), never through a
|
|
104
|
-
third-party tool; reading another tool's docs or a dry run to learn a path is fine.
|
|
105
|
-
3. Test in a throwaway group the bot creates for it (`zz-test ...`), never in a
|
|
106
|
-
terminal's group; label each test message ("Test 2/3: ...") so the user can tell
|
|
107
|
-
which one rang.
|
|
108
|
-
4. Measure the result (an API field, a read-back) instead of assuming it; ask the user
|
|
109
|
-
only what only a phone shows (did it ring).
|
|
110
|
-
5. Clean up in the same run: disband the test group, delete test labels and tags you
|
|
111
|
-
made, recall test messages, delete scratch files.
|
|
112
|
-
|
|
113
|
-
## Before a commit
|
|
114
|
-
|
|
115
|
-
1. `npm test` passes.
|
|
116
|
-
2. Stage, then, as its own step, scan the staged files for anything real: session and
|
|
117
|
-
machine names, the app id (`cli_`), open_ids (`ou_`), chat ids (`oc_`), message ids
|
|
118
|
-
(`om_`), label ids (`ofg_`), the tenant's name, people's names, local paths. Read the
|
|
119
|
-
result before committing; never chain the scan and the commit in one command.
|
|
120
|
-
3. Show the commit message (Conventional Commits; the body says why) and commit only
|
|
121
|
-
after the user's yes. No push without its own yes.
|
|
91
|
+
| `herdrchat start` says it did not reach its chat app | wrong keys, or no network | `herdrchat logs --errors`; check the keys in `config.env` |
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
A Telegram bot cannot make groups (the Bot API has no such method), so herdrchat puts each
|
|
4
4
|
herdr-app tab in a **topic** of your private chat with the bot: each has its own name,
|
|
5
5
|
icon color (one per space), unread count, and can be muted on its own. The chat's main
|
|
6
|
-
thread holds the overview and the commands.
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
thread holds the overview and the commands. When Telegram is the only chat app, it uses
|
|
7
|
+
the default profile; beside a Feishu bot it gets its own (`--profile telegram`), and both
|
|
8
|
+
follow the same hc terminals, so an answer in either reaches the terminal and a dialog
|
|
9
|
+
answered in one closes its card in the other. Below, `P` is ` --profile <name>` when it
|
|
10
|
+
has its own profile, else nothing.
|
|
9
11
|
|
|
10
12
|
## Setup (`/herdrchat telegram setup`)
|
|
11
13
|
|
|
@@ -14,34 +16,26 @@ Do these in order; the user does the Telegram steps, you check each one.
|
|
|
14
16
|
1. **The bot.** The user messages **@BotFather**: `/newbot`, a name, a username ending in
|
|
15
17
|
`bot`. BotFather answers with a token. The user keeps it: never ask them to paste it
|
|
16
18
|
into the chat.
|
|
17
|
-
2. **
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
3. **
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`
|
|
37
|
-
6. **Start again.** Read the config (secrets hidden) in one step, then
|
|
38
|
-
`herdrchat start --profile telegram` in the next. One topic per tab appears; Telegram
|
|
39
|
-
takes about one message a second in a chat, so twenty tabs take twenty seconds. A new
|
|
40
|
-
topic gets the conversation before it (`HERDR_HISTORY`); `/history` in a topic posts it
|
|
41
|
-
again.
|
|
42
|
-
7. **Make it a service.** Once it works: `herdrchat install --profile telegram`. It then
|
|
43
|
-
starts at login and comes back by itself after a crash, whatever happens to the hc
|
|
44
|
-
cores; `herdrchat status --profile telegram` says it runs as the service.
|
|
19
|
+
2. **The config.** `herdrchat init --chat telegram P` makes the profile's `config.env`
|
|
20
|
+
(private) and prints where it is. Ask the user to paste the token after
|
|
21
|
+
`TELEGRAM_BOT_TOKEN=` themselves (`open -e <file>` opens it), no quotes, no spaces, and
|
|
22
|
+
fill `HERDR_SESSIONS` with them (`ls ~/.local/state/herdr-core` lists the sessions).
|
|
23
|
+
3. **Topic mode, checked before the first start.** It is not in BotFather's `/mybots` →
|
|
24
|
+
Bot Settings list; it is in BotFather's mini app (the **Open** button at the bottom
|
|
25
|
+
left of the BotFather chat): pick the bot, turn on Threaded Mode. Check it without
|
|
26
|
+
printing the token: a short `node -e` reads the token from the config, calls
|
|
27
|
+
`https://api.telegram.org/bot<token>/getMe` and prints only `username` and
|
|
28
|
+
`has_topics_enabled`, which must be `true`.
|
|
29
|
+
4. **First start, to learn the owner.** `herdrchat start P`; the user messages the bot
|
|
30
|
+
anything; it answers with the line to add, `TELEGRAM_OWNER_ID=<digits>`, and where the
|
|
31
|
+
file is. The user adds it. Until then the bot makes no topic.
|
|
32
|
+
5. **Start again.** Read the config (secrets hidden) in one step, then
|
|
33
|
+
`herdrchat start P` in the next. One topic per tab appears; Telegram takes about one
|
|
34
|
+
message a second in a chat, so twenty tabs take twenty seconds. A new topic gets the
|
|
35
|
+
conversation before it (`HERDR_HISTORY`); `/history` in a topic posts it again.
|
|
36
|
+
6. **Make it a service.** Once it works: `herdrchat install P`. It then starts at login
|
|
37
|
+
and comes back by itself after a crash, whatever happens to the hc cores;
|
|
38
|
+
`herdrchat status P` says it runs as the service.
|
|
45
39
|
|
|
46
40
|
## What rings
|
|
47
41
|
|
|
@@ -64,7 +58,8 @@ without muting each topic by hand.
|
|
|
64
58
|
| an ended group gets the "ended" tag | an ended topic's name starts with 🔚 (closing a topic is documented for supergroups only) |
|
|
65
59
|
| an image inside a card | each image as its own photo after the text |
|
|
66
60
|
| a text box on a dialog card | type the answer in the topic |
|
|
67
|
-
|
|
|
61
|
+
| pins the group that needs you | pins the dialog card in its topic until it is answered |
|
|
62
|
+
| tags, labels, `/auth` | none (a bot cannot act as the user) |
|
|
68
63
|
| the overview card | one message in the main thread, edited in place |
|
|
69
64
|
| `/` commands: typed | `/` shows a menu: ls, new, kill, screen, answer, esc, history |
|
|
70
65
|
|
|
@@ -72,9 +67,9 @@ without muting each topic by hand.
|
|
|
72
67
|
|
|
73
68
|
| Seen | Cause | Do |
|
|
74
69
|
|---|---|---|
|
|
75
|
-
| log: `topic mode is off for @<bot>` | step
|
|
76
|
-
| the bot answers
|
|
77
|
-
| `herdrchat start` says it did not reach its chat app | wrong token, or no network | check with getMe (step
|
|
78
|
-
| log: `telegram poll: ... 409` | another copy polls the same token | `herdrchat stop
|
|
70
|
+
| log: `topic mode is off for @<bot>` | step 3 not done | turn it on in BotFather's mini app, check `has_topics_enabled`, start again |
|
|
71
|
+
| the bot answers "To make you this bot's owner…" | `TELEGRAM_OWNER_ID` empty | step 4 |
|
|
72
|
+
| `herdrchat start` says it did not reach its chat app | wrong token, or no network | check with getMe (step 3) |
|
|
73
|
+
| log: `telegram poll: ... 409` | another copy polls the same token | `herdrchat stop P`, find the other one (another machine?), start once |
|
|
79
74
|
| log: `too fast, waiting N s` | Telegram's flood limit; the bot waits as told | nothing; it catches up |
|
|
80
75
|
| a click answers "This dialog is gone" | the card is older than the last 300 dialog cards | answer the newest card, or in the terminal |
|