@vicfei/dsh-super-agent 0.7.0 → 0.8.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.
@@ -0,0 +1,134 @@
1
+ // Telegram channel (v0.8.1, issue #51) — the official Bot API, the simplest of the five
2
+ // channels: one token from @BotFather, documented HTTP long-poll (getUpdates + offset cursor)
3
+ // and sendMessage. The bot's own username comes from getMe, so group @mention detection is
4
+ // precise with zero manual config (the Slack auth.test pattern). Bots never receive their own
5
+ // sends — the echo filter remains as the second layer.
6
+ export const TELEGRAM_DEFAULT_BASE = "https://api.telegram.org";
7
+ export class TelegramClient {
8
+ fetchImpl;
9
+ base;
10
+ token;
11
+ constructor(opts) {
12
+ this.fetchImpl = opts.fetchImpl ?? fetch;
13
+ this.base = (opts.baseUrl ?? TELEGRAM_DEFAULT_BASE).replace(/\/$/, "");
14
+ this.token = opts.token;
15
+ }
16
+ async call(method, body, signal) {
17
+ const res = await this.fetchImpl(`${this.base}/bot${this.token}/${method}`, {
18
+ method: "POST",
19
+ signal,
20
+ headers: { "content-type": "application/json" },
21
+ body: JSON.stringify(body),
22
+ });
23
+ if (!res.ok)
24
+ throw new Error(`telegram ${method} → HTTP ${res.status}`);
25
+ const data = (await res.json());
26
+ if (data.ok === false)
27
+ throw new Error(`telegram ${method}: ${data.description ?? "not ok"}`);
28
+ return data;
29
+ }
30
+ getMe() {
31
+ return this.call("getMe", {});
32
+ }
33
+ getUpdates(offset, timeoutSec, signal) {
34
+ return this.call("getUpdates", { offset, timeout: timeoutSec, allowed_updates: ["message"] }, signal);
35
+ }
36
+ sendMessage(chatId, text) {
37
+ return this.call("sendMessage", { chat_id: chatId, text });
38
+ }
39
+ }
40
+ // ── trigger mapping ─────────────────────────────────────────────────────────────
41
+ export function telegramTriggerFromMessage(m, botUsername) {
42
+ const chatId = m.chat?.id;
43
+ const msgId = m.message_id;
44
+ if (chatId === undefined || msgId === undefined)
45
+ return null;
46
+ const isPrivate = m.chat?.type === "private";
47
+ const text = m.text ?? m.caption ?? "";
48
+ const addressing = isPrivate ? true // a DM is inherently addressed
49
+ : botUsername ? (m.text ?? "").includes(`@${botUsername}`)
50
+ : false; // unknown bot username → fail closed (inferred via listedNotAddressing remains)
51
+ const login = m.from?.username ?? (m.from?.id !== undefined ? String(m.from.id) : "unknown");
52
+ return {
53
+ id: `telegram:msg/${chatId}/${msgId}`,
54
+ source: "telegram",
55
+ scope: isPrivate ? "im_dm" : "im_group",
56
+ threadId: `telegram:chat/${chatId}`,
57
+ author: { login, isBot: m.from?.is_bot === true },
58
+ addressing,
59
+ body: m.text !== undefined ? text : text ? `[caption] ${text}` : "",
60
+ ts: m.date ? new Date(m.date * 1000).toISOString() : new Date().toISOString(),
61
+ };
62
+ }
63
+ export class TelegramSource {
64
+ opts;
65
+ kind = "telegram";
66
+ botUsername;
67
+ me = null;
68
+ constructor(opts) {
69
+ this.opts = opts;
70
+ }
71
+ discoverMe() {
72
+ this.me ??= (async () => {
73
+ const client = new TelegramClient({ token: this.opts.token, ...(this.opts.baseUrl ? { baseUrl: this.opts.baseUrl } : {}), ...(this.opts.fetchImpl ? { fetchImpl: this.opts.fetchImpl } : {}) });
74
+ try {
75
+ const me = await client.getMe();
76
+ this.botUsername = me.result?.username;
77
+ this.opts.log?.(`[telegram] bot @${this.botUsername ?? "?"} — group @mention detection ${this.botUsername ? "armed" : "unavailable (fail-closed)"}`);
78
+ }
79
+ catch (e) {
80
+ this.opts.log?.(`[telegram] getMe failed: ${e instanceof Error ? e.message : e} — group @mentions fail closed until it succeeds`);
81
+ }
82
+ })();
83
+ return this.me;
84
+ }
85
+ async collect(from, opts = {}) {
86
+ await this.discoverMe();
87
+ const client = new TelegramClient({ token: this.opts.token, ...(this.opts.baseUrl ? { baseUrl: this.opts.baseUrl } : {}), ...(this.opts.fetchImpl ? { fetchImpl: this.opts.fetchImpl } : {}) });
88
+ const abort = new AbortController();
89
+ const timer = setTimeout(() => abort.abort(), this.opts.pollTimeoutMs ?? 8000);
90
+ let updates;
91
+ try {
92
+ const res = await client.getUpdates(this.opts.loadOffset(), Math.floor((this.opts.pollTimeoutMs ?? 8000) / 1000), abort.signal);
93
+ updates = res.result ?? [];
94
+ }
95
+ catch (e) {
96
+ if (abort.signal.aborted)
97
+ return { triggers: [], next: from }; // our own timeout — offset hasn't moved, lossless
98
+ throw e;
99
+ }
100
+ finally {
101
+ clearTimeout(timer);
102
+ }
103
+ const triggers = [];
104
+ for (const u of updates) {
105
+ if (u.update_id !== undefined)
106
+ this.opts.saveOffset(u.update_id + 1);
107
+ const t = u.message ? telegramTriggerFromMessage(u.message, this.botUsername) : null;
108
+ if (t && !opts.exclude?.has(t.id))
109
+ triggers.push(t);
110
+ }
111
+ return { triggers, next: from };
112
+ }
113
+ }
114
+ // ── ReplyChannel ────────────────────────────────────────────────────────────────
115
+ export class TelegramReplyChannel {
116
+ clientOf;
117
+ chatIdOf;
118
+ constructor(clientOf, chatIdOf) {
119
+ this.clientOf = clientOf;
120
+ this.chatIdOf = chatIdOf;
121
+ }
122
+ matches(threadId) {
123
+ return threadId.startsWith("telegram:");
124
+ }
125
+ async deliver(threadId, body) {
126
+ const chatId = this.chatIdOf(threadId);
127
+ const res = await this.clientOf().sendMessage(chatId, body);
128
+ const msgId = res.result?.message_id;
129
+ if (msgId === undefined)
130
+ throw new Error("telegram sendMessage returned no message_id");
131
+ return { externalId: `msg/${chatId}/${msgId}` };
132
+ }
133
+ }
134
+ //# sourceMappingURL=telegram.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telegram.js","sourceRoot":"","sources":["../src/telegram.ts"],"names":[],"mappings":"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"}
package/dist/usage.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const USAGE = "dsh-super-agent \u2014 admission control, observe mode & tickets for any CLI coding agent\n\nUsage: dsh-super-agent <command> [options]\n\nCommands:\n poll one pass: poll GitHub \u2192 gate \u2192 (assessment) \u2192 record; --post also drains queued replies\n work claim queued tickets \u2192 run the executor in a worktree \u2192 report\n watch poll + work + panel as one daemon (Ctrl-C stops after the current cycle)\n tickets list tickets; \"tickets stop <id>\" ends one\n panel serve the awaiting-human panel (http://127.0.0.1:8787)\n merge sweep open ticket PRs \u2014 merge those meeting policy.merge requirements (default off)\n help show this usage\n\nCommon options:\n --repo owner/name repository to watch\n --agent-login <login> the agent's GitHub login (mention detection)\n --config <file> policy config (default policy.json, auto-scaffolded from examples/)\n --executor-config <file> executor config (default executor.json, auto-scaffolded from examples/)\n --db <file> state database (default .state/engine.db)\n --token <token> GitHub token (or env GITHUB_TOKEN)\n --assessment <mode> stub-positive | stub-negative | llm\n --interval <seconds> watch poll interval (\u00B120% jitter)\n --no-panel watch without the panel server\n --post poll also posts queued replies (needs a token)\n\nEnvironment: GITHUB_TOKEN, or GitHub App auth (GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY[_PATH],\noptional GITHUB_APP_INSTALLATION_ID), ASSESS_BASE_URL, ASSESS_MODEL, ASSESS_API_KEY, PANEL_PORT.\nDocs: https://github.com/vicfei/dsh-super-agent#quick-start";
1
+ export declare const USAGE = "dsh-super-agent \u2014 admission control, observe mode & tickets for any CLI coding agent\n\nUsage: dsh-super-agent <command> [options]\n\nCommands:\n poll one pass: poll GitHub \u2192 gate \u2192 (assessment) \u2192 record; --post also drains queued replies\n work claim queued tickets \u2192 run the executor in a worktree \u2192 report\n watch poll + work + panel as one daemon (Ctrl-C stops after the current cycle)\n tickets list tickets; \"tickets stop <id>\" ends one\n panel serve the awaiting-human panel (http://127.0.0.1:8787)\n merge sweep open ticket PRs \u2014 merge those meeting policy.merge requirements (default off)\n wechat-login iLink QR login: scan with WeChat, credentials land in the engine db\n help show this usage\n\nCommon options:\n --repo owner/name repository to watch\n --agent-login <login> the agent's GitHub login (mention detection)\n --config <file> policy config (default policy.json, auto-scaffolded from examples/)\n --executor-config <file> executor config (default executor.json, auto-scaffolded from examples/)\n --db <file> state database (default .state/engine.db)\n --token <token> GitHub token (or env GITHUB_TOKEN)\n --assessment <mode> stub-positive | stub-negative | llm\n --interval <seconds> watch poll interval (\u00B120% jitter)\n --no-panel watch without the panel server\n --post poll also posts queued replies (needs a token)\n\nEnvironment: GITHUB_TOKEN, or GitHub App auth (GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY[_PATH],\noptional GITHUB_APP_INSTALLATION_ID); Feishu channel (FEISHU_APP_ID + FEISHU_APP_SECRET,\noptional FEISHU_BOT_OPEN_ID); Slack channel (SLACK_BOT_TOKEN + SLACK_APP_TOKEN);\nTelegram channel (TELEGRAM_BOT_TOKEN); ASSESS_BASE_URL, ASSESS_MODEL, ASSESS_API_KEY, PANEL_PORT.\nDocs: https://github.com/vicfei/dsh-super-agent#quick-start";
2
2
  export declare function examplePath(name: string): string | null;
3
3
  /** Copy the packaged example to `target` when neither exists yet. True ⇒ caller should tell the user to edit & rerun. */
4
4
  export declare function scaffoldFromExample(target: string, example: string): boolean;
package/dist/usage.js CHANGED
@@ -15,6 +15,7 @@ Commands:
15
15
  tickets list tickets; "tickets stop <id>" ends one
16
16
  panel serve the awaiting-human panel (http://127.0.0.1:8787)
17
17
  merge sweep open ticket PRs — merge those meeting policy.merge requirements (default off)
18
+ wechat-login iLink QR login: scan with WeChat, credentials land in the engine db
18
19
  help show this usage
19
20
 
20
21
  Common options:
@@ -30,7 +31,9 @@ Common options:
30
31
  --post poll also posts queued replies (needs a token)
31
32
 
32
33
  Environment: GITHUB_TOKEN, or GitHub App auth (GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY[_PATH],
33
- optional GITHUB_APP_INSTALLATION_ID), ASSESS_BASE_URL, ASSESS_MODEL, ASSESS_API_KEY, PANEL_PORT.
34
+ optional GITHUB_APP_INSTALLATION_ID); Feishu channel (FEISHU_APP_ID + FEISHU_APP_SECRET,
35
+ optional FEISHU_BOT_OPEN_ID); Slack channel (SLACK_BOT_TOKEN + SLACK_APP_TOKEN);
36
+ Telegram channel (TELEGRAM_BOT_TOKEN); ASSESS_BASE_URL, ASSESS_MODEL, ASSESS_API_KEY, PANEL_PORT.
34
37
  Docs: https://github.com/vicfei/dsh-super-agent#quick-start`;
35
38
  const examplesDir = join(dirname(fileURLToPath(import.meta.url)), "..", "examples");
36
39
  export function examplePath(name) {
package/dist/usage.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,4FAA4F;AAC5F,yEAAyE;AAEzE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,CAAC,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;4DA2BuC,CAAC;AAE7D,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;AAEpF,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;IAClC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAClC,CAAC;AAED,yHAAyH;AACzH,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,OAAe;IACjE,MAAM,GAAG,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,IAAI,CAAC,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7C,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC1B,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAG;;;uGAG0E,CAAC"}
1
+ {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,4FAA4F;AAC5F,yEAAyE;AAEzE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,CAAC,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4DA8BuC,CAAC;AAE7D,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;AAEpF,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;IAClC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAClC,CAAC;AAED,yHAAyH;AACzH,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,OAAe;IACjE,MAAM,GAAG,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,IAAI,CAAC,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7C,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC1B,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAG;;;uGAG0E,CAAC"}
package/dist/watch.d.ts CHANGED
@@ -1,14 +1,17 @@
1
1
  import type { Policy, Trigger } from "./domain.js";
2
2
  import type { Engine } from "./engine.js";
3
- import type { GitHubSource } from "./github/source.js";
3
+ import type { ReplyChannel, Source } from "./source.js";
4
4
  import type { Executor } from "./executor.js";
5
5
  import type { GitHubClient } from "./github/client.js";
6
6
  export type AssessmentFn = (t: Trigger) => Promise<"positive" | "negative"> | "positive" | "negative";
7
7
  export interface WatchOptions {
8
8
  engine: Engine;
9
- source: GitHubSource;
9
+ /** All sources, polled each cycle (v0.8); each keeps its own read state in the engine. */
10
+ sources: Source[];
10
11
  executor: Executor;
11
12
  client: GitHubClient | null;
13
+ /** Reply routes by threadId prefix (v0.8). */
14
+ channels: ReplyChannel[];
12
15
  repo: string;
13
16
  policy: Policy;
14
17
  assessment: AssessmentFn;
package/dist/watch.js CHANGED
@@ -32,21 +32,25 @@ export async function runWatch(opts) {
32
32
  }
33
33
  }
34
34
  async function runCycle(opts, log) {
35
- const { engine, source, executor, client, repo, policy } = opts;
36
- const { triggers, next } = await source.collect(engine.positions(), { exclude: engine.selfPosted() });
35
+ const { engine, executor, client, channels, repo, policy } = opts;
37
36
  const batch = [];
38
- for (const t of triggers)
39
- batch.push(classify(t, policy, await opts.assessment(t)));
37
+ const nextBySource = {};
38
+ for (const source of opts.sources) {
39
+ const { triggers, next } = await source.collect(engine.sourceState(source.kind), { exclude: engine.selfPosted() });
40
+ nextBySource[source.kind] = next;
41
+ for (const t of triggers)
42
+ batch.push(classify(t, policy, await opts.assessment(t)));
43
+ }
40
44
  for (const outcome of batch)
41
45
  log(formatOutcome(outcome));
42
- const report = engine.ingest(batch, next);
46
+ const report = engine.ingest(batch, nextBySource);
43
47
  for (const refusal of report.refusals)
44
48
  log(`[REFUSED] ${refusal}`);
45
- const reports = await runWork(engine, executor, repo, client);
49
+ const reports = await runWork(engine, executor, repo, client, channels);
46
50
  for (const r of reports)
47
51
  log(`#${r.ticketId} [${r.outcome}] ${r.detail}`);
48
52
  // Auto-merge sweep (M6-1) — no-op unless policy.merge.enabled; structural brakes inside.
49
- const merged = client ? await runMergeSweep(engine, client, repo, policy, (l) => log(`[merge] ${l}`)) : [];
53
+ const merged = client ? await runMergeSweep(engine, client, repo, policy, channels, (l) => log(`[merge] ${l}`)) : [];
50
54
  if (batch.length > 0 || reports.length > 0 || report.refusals.length > 0 || merged.length > 0) {
51
55
  log(`— cycle: fresh=${report.fresh} opened=${report.ticketsOpened} resumed=${report.resumed} joined=${report.joined} worked=${reports.length} merged=${merged.length}`);
52
56
  return true;
package/dist/watch.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"watch.js","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"watch.js","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,qFAAqF;AACrF,mFAAmF;AACnF,iEAAiE;AAOjE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoBpC,MAAM,cAAc,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAEtC,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAkB;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACxD,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,IAAI,MAAM,CAAC;IACvC,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,IAAI,GAAG,GAAG,EAAE,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACvC,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC7B,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAE9B,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;QAC7F,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAC5C,SAAS,GAAG,CAAC,CAAC,CAAC,kDAAkD;YACjE,IAAI,SAAS;gBAAE,SAAS,CAAC,qDAAqD;QAChF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,SAAS,GAAG,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,GAAG,CAAC,EAAE,cAAc,CAAC,CAAC;YAC7E,GAAG,CAAC,iBAAiB,KAAK,YAAY,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,kBAAkB,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7H,CAAC;QACD,IAAI,OAAO,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,IAAI,IAAI,CAAC,MAAM;YAAE,MAAM;QACxE,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC;QAC/C,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACpF,CAAC;AACH,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,IAAkB,EAAE,GAAwB;IAClE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAElE,MAAM,KAAK,GAAG,EAAE,CAAC;IACjB,MAAM,YAAY,GAAgC,EAAE,CAAC;IACrD,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QAClC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QACnH,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;QACjC,KAAK,MAAM,CAAC,IAAI,QAAQ;YAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtF,CAAC;IACD,KAAK,MAAM,OAAO,IAAI,KAAK;QAAE,GAAG,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IACzD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IAClD,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ;QAAE,GAAG,CAAC,aAAa,OAAO,EAAE,CAAC,CAAC;IAEnE,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IACxE,KAAK,MAAM,CAAC,IAAI,OAAO;QAAE,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IAE1E,yFAAyF;IACzF,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAErH,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9F,GAAG,CAAC,kBAAkB,MAAM,CAAC,KAAK,WAAW,MAAM,CAAC,aAAa,YAAY,MAAM,CAAC,OAAO,WAAW,MAAM,CAAC,MAAM,WAAW,OAAO,CAAC,MAAM,WAAW,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;QACxK,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,142 @@
1
+ import type { Trigger } from "./domain.js";
2
+ import type { ReplyChannel, Source, SourceState } from "./source.js";
3
+ export declare const WECHAT_DEFAULT_BASE = "https://ilinkai.weixin.qq.com";
4
+ export interface WeixinMessage {
5
+ message_id?: string;
6
+ seq?: number;
7
+ from_user_id?: string;
8
+ to_user_id?: string;
9
+ session_id?: string;
10
+ group_id?: string;
11
+ /** 1 = USER, 2 = BOT. */
12
+ message_type?: number;
13
+ create_time_ms?: number;
14
+ item_list?: {
15
+ type?: number;
16
+ text_item?: {
17
+ text?: string;
18
+ };
19
+ }[];
20
+ context_token?: string;
21
+ }
22
+ export interface GetUpdatesResp {
23
+ ret?: number;
24
+ errcode?: number;
25
+ errmsg?: string;
26
+ msgs?: WeixinMessage[];
27
+ get_updates_buf?: string;
28
+ }
29
+ export interface SendMessageResp {
30
+ ret?: number;
31
+ errmsg?: string;
32
+ message_id?: string;
33
+ }
34
+ export interface WechatAuth {
35
+ token: string;
36
+ baseUrl: string;
37
+ }
38
+ export interface WechatRoute {
39
+ toUserId: string;
40
+ groupId?: string;
41
+ sessionId?: string;
42
+ contextToken: string;
43
+ }
44
+ export interface WechatStore {
45
+ loadAuth(): WechatAuth | null;
46
+ saveAuth(auth: WechatAuth): void;
47
+ loadCursor(): string;
48
+ saveCursor(buf: string): void;
49
+ loadRoute(threadId: string): WechatRoute | null;
50
+ saveRoute(threadId: string, route: WechatRoute): void;
51
+ /** Session expired (-14): drop everything, a new QR scan is required. */
52
+ clearAll(): void;
53
+ }
54
+ export declare class MemoryWechatStore implements WechatStore {
55
+ private auth;
56
+ private cursor;
57
+ private readonly routes;
58
+ loadAuth(): WechatAuth | null;
59
+ saveAuth(auth: WechatAuth): void;
60
+ loadCursor(): string;
61
+ saveCursor(buf: string): void;
62
+ loadRoute(threadId: string): WechatRoute | null;
63
+ saveRoute(threadId: string, route: WechatRoute): void;
64
+ clearAll(): void;
65
+ }
66
+ /** Production store over the engine's kv (same db as the tickets — one backup story). */
67
+ export declare class EngineWechatStore implements WechatStore {
68
+ private readonly kv;
69
+ constructor(kv: {
70
+ kvGet(k: string): string | null;
71
+ kvSet(k: string, v: string): void;
72
+ kvDelete(k: string): void;
73
+ });
74
+ loadAuth(): WechatAuth | null;
75
+ saveAuth(auth: WechatAuth): void;
76
+ loadCursor(): string;
77
+ saveCursor(buf: string): void;
78
+ loadRoute(threadId: string): WechatRoute | null;
79
+ saveRoute(threadId: string, route: WechatRoute): void;
80
+ clearAll(): void;
81
+ }
82
+ export interface WechatClientOptions {
83
+ token: string;
84
+ baseUrl?: string;
85
+ apiBase?: string;
86
+ fetchImpl?: typeof fetch;
87
+ }
88
+ export declare class WechatApiClient {
89
+ private readonly fetchImpl;
90
+ private readonly base;
91
+ private readonly token;
92
+ constructor(opts: WechatClientOptions);
93
+ private post;
94
+ getUpdates(getUpdatesBuf: string, signal?: AbortSignal): Promise<GetUpdatesResp>;
95
+ sendMessage(msg: WeixinMessage): Promise<SendMessageResp>;
96
+ }
97
+ export interface QrPayload {
98
+ qrcode: string;
99
+ imgBase64: string;
100
+ }
101
+ /**
102
+ * QR login flow: fetch a QR (the caller renders/saves the image — it must be scanned by the
103
+ * human's WeChat app), long-poll status until confirmed, return credentials.
104
+ */
105
+ export declare function wechatLogin(onQr: (qr: QrPayload) => void, opts?: {
106
+ apiBase?: string;
107
+ fetchImpl?: typeof fetch;
108
+ pollMs?: number;
109
+ }): Promise<WechatAuth>;
110
+ /** threadId for a message: groups are threads; a DM peer is a thread. */
111
+ export declare function wechatThreadId(m: WeixinMessage): string;
112
+ export declare function wechatTriggerFromMessage(m: WeixinMessage): Trigger | null;
113
+ export interface WechatSourceOptions {
114
+ store: WechatStore;
115
+ /** Abort the long-poll after this many ms so a watch cycle is never blocked (default 8s). */
116
+ pollTimeoutMs?: number;
117
+ apiBase?: string;
118
+ fetchImpl?: typeof fetch;
119
+ log?: (l: string) => void;
120
+ }
121
+ export declare class WechatSource implements Source {
122
+ private readonly opts;
123
+ readonly kind: "wechat";
124
+ private warnedNoAuth;
125
+ constructor(opts: WechatSourceOptions);
126
+ collect(from: SourceState, opts?: {
127
+ exclude?: ReadonlySet<string>;
128
+ }): Promise<{
129
+ triggers: Trigger[];
130
+ next: SourceState;
131
+ }>;
132
+ }
133
+ export declare class WechatReplyChannel implements ReplyChannel {
134
+ private readonly store;
135
+ private readonly clientOf;
136
+ private readonly log;
137
+ constructor(store: WechatStore, clientOf: (auth: WechatAuth) => WechatApiClient, log?: (l: string) => void);
138
+ matches(threadId: string): boolean;
139
+ deliver(threadId: string, body: string): Promise<{
140
+ externalId: string;
141
+ }>;
142
+ }
package/dist/wechat.js ADDED
@@ -0,0 +1,262 @@
1
+ // WeChat channel (v0.8 M8-4, issue #45) — the iLink Bot Protocol: an HTTP, Telegram-bot-shaped
2
+ // API on WeChat's own domain (ilinkai.weixin.qq.com), documented at wechatbot.dev/zh/protocol
3
+ // and mirrored by the wire types of weixin-agent-sdk (derived from @tencent-weixin/openclaw-weixin).
4
+ // Zero reverse-engineering: QR login → bot_token, long-poll getupdates with an opaque cursor,
5
+ // sendmessage echoing the per-peer context_token. Text-first; media/AES is a later slice.
6
+ //
7
+ // Persistence is injected (Engine kv in production, in-memory in tests): credentials, the
8
+ // getupdates cursor, and per-thread reply routes {toUserId, groupId, sessionId, contextToken}.
9
+ import { randomBytes } from "node:crypto";
10
+ export const WECHAT_DEFAULT_BASE = "https://ilinkai.weixin.qq.com";
11
+ export class MemoryWechatStore {
12
+ auth = null;
13
+ cursor = "";
14
+ routes = new Map();
15
+ loadAuth() { return this.auth; }
16
+ saveAuth(auth) { this.auth = auth; }
17
+ loadCursor() { return this.cursor; }
18
+ saveCursor(buf) { this.cursor = buf; }
19
+ loadRoute(threadId) { return this.routes.get(threadId) ?? null; }
20
+ saveRoute(threadId, route) { this.routes.set(threadId, route); }
21
+ clearAll() { this.auth = null; this.cursor = ""; this.routes.clear(); }
22
+ }
23
+ /** Production store over the engine's kv (same db as the tickets — one backup story). */
24
+ export class EngineWechatStore {
25
+ kv;
26
+ constructor(kv) {
27
+ this.kv = kv;
28
+ }
29
+ loadAuth() {
30
+ try {
31
+ const raw = this.kv.kvGet("wechat_auth");
32
+ return raw ? JSON.parse(raw) : null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ saveAuth(auth) {
39
+ this.kv.kvSet("wechat_auth", JSON.stringify(auth));
40
+ }
41
+ loadCursor() {
42
+ return this.kv.kvGet("wechat_cursor") ?? "";
43
+ }
44
+ saveCursor(buf) {
45
+ this.kv.kvSet("wechat_cursor", buf);
46
+ }
47
+ loadRoute(threadId) {
48
+ try {
49
+ const raw = this.kv.kvGet(`wechat_route:${threadId}`);
50
+ return raw ? JSON.parse(raw) : null;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ saveRoute(threadId, route) {
57
+ this.kv.kvSet(`wechat_route:${threadId}`, JSON.stringify(route));
58
+ const index = new Set((this.kv.kvGet("wechat_route_index") ?? "").split("\n").filter(Boolean));
59
+ index.add(threadId);
60
+ this.kv.kvSet("wechat_route_index", [...index].join("\n"));
61
+ }
62
+ clearAll() {
63
+ // route keys are namespaced; policy/positions keys are unaffected
64
+ this.kv.kvGet("wechat_route_index")?.split("\n").filter(Boolean).forEach((k) => this.kv.kvDelete(`wechat_route:${k}`));
65
+ this.kv.kvSet("wechat_route_index", "");
66
+ this.kv.kvDelete("wechat_auth");
67
+ this.kv.kvDelete("wechat_cursor");
68
+ }
69
+ }
70
+ // ── API client ──────────────────────────────────────────────────────────────────
71
+ /** X-WECHAT-UIN: random uint32 → decimal string → base64, fresh per request. */
72
+ function randomWechatUin() {
73
+ return Buffer.from(String(randomBytes(4).readUInt32BE(0) >>> 0)).toString("base64");
74
+ }
75
+ const BOT_AGENT = "dsh-super-agent/0.8.0";
76
+ export class WechatApiClient {
77
+ fetchImpl;
78
+ base;
79
+ token;
80
+ constructor(opts) {
81
+ this.fetchImpl = opts.fetchImpl ?? fetch;
82
+ this.base = (opts.baseUrl ?? opts.apiBase ?? WECHAT_DEFAULT_BASE).replace(/\/$/, "");
83
+ this.token = opts.token;
84
+ }
85
+ async post(path, body, signal) {
86
+ const res = await this.fetchImpl(`${this.base}/${path}`, {
87
+ method: "POST",
88
+ signal,
89
+ headers: {
90
+ "content-type": "application/json",
91
+ AuthorizationType: "ilink_bot_token",
92
+ Authorization: `Bearer ${this.token}`,
93
+ "X-WECHAT-UIN": randomWechatUin(),
94
+ },
95
+ body: JSON.stringify({ ...body, base_info: { channel_version: "2.0.0", bot_agent: BOT_AGENT } }),
96
+ });
97
+ if (!res.ok)
98
+ throw new Error(`wechat ${path} → HTTP ${res.status}`);
99
+ return (await res.json());
100
+ }
101
+ getUpdates(getUpdatesBuf, signal) {
102
+ return this.post("ilink/bot/getupdates", { get_updates_buf: getUpdatesBuf }, signal);
103
+ }
104
+ sendMessage(msg) {
105
+ return this.post("ilink/bot/sendmessage", { msg });
106
+ }
107
+ }
108
+ /**
109
+ * QR login flow: fetch a QR (the caller renders/saves the image — it must be scanned by the
110
+ * human's WeChat app), long-poll status until confirmed, return credentials.
111
+ */
112
+ export async function wechatLogin(onQr, opts = {}) {
113
+ const fetchImpl = opts.fetchImpl ?? fetch;
114
+ const base = (opts.apiBase ?? WECHAT_DEFAULT_BASE).replace(/\/$/, "");
115
+ const headers = {
116
+ "content-type": "application/json",
117
+ AuthorizationType: "ilink_bot_token",
118
+ "X-WECHAT-UIN": randomWechatUin(),
119
+ };
120
+ const qrRes = await fetchImpl(`${base}/ilink/bot/get_bot_qrcode?bot_type=3`, {
121
+ method: "POST",
122
+ headers,
123
+ body: JSON.stringify({ local_token_list: [] }),
124
+ });
125
+ if (!qrRes.ok)
126
+ throw new Error(`wechat get_bot_qrcode → HTTP ${qrRes.status}`);
127
+ const qr = (await qrRes.json());
128
+ if (!qr.qrcode)
129
+ throw new Error("wechat get_bot_qrcode returned no qrcode");
130
+ onQr({ qrcode: qr.qrcode, imgBase64: qr.qrcode_img_content ?? "" });
131
+ for (;;) {
132
+ await new Promise((r) => setTimeout(r, opts.pollMs ?? 2000));
133
+ const stRes = await fetchImpl(`${base}/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qr.qrcode)}`, { headers });
134
+ if (!stRes.ok)
135
+ throw new Error(`wechat get_qrcode_status → HTTP ${stRes.status}`);
136
+ const st = (await stRes.json());
137
+ if (st.status === "confirmed" && st.bot_token) {
138
+ return { token: st.bot_token, baseUrl: st.baseurl ?? base };
139
+ }
140
+ if (st.status === "expired")
141
+ throw new Error("wechat QR expired — run wechat-login again");
142
+ // wait | scaned → keep polling
143
+ }
144
+ }
145
+ // ── trigger mapping ─────────────────────────────────────────────────────────────
146
+ /** threadId for a message: groups are threads; a DM peer is a thread. */
147
+ export function wechatThreadId(m) {
148
+ return m.group_id ? `wechat:group/${m.group_id}` : `wechat:peer/${m.from_user_id ?? "unknown"}`;
149
+ }
150
+ export function wechatTriggerFromMessage(m) {
151
+ if (m.message_type === 2)
152
+ return null; // BOT traffic (incl. our own) — never a work request
153
+ if (!m.message_id && m.seq === undefined)
154
+ return null;
155
+ const firstText = m.item_list?.find((i) => i.type === 1)?.text_item?.text;
156
+ const anyItem = m.item_list?.[0];
157
+ const body = firstText ?? (anyItem?.type !== undefined && anyItem.type !== 1 ? `[${anyItem.type}]` : "");
158
+ const isGroup = m.group_id !== undefined;
159
+ return {
160
+ id: `wechat:msg/${m.message_id ?? `seq-${m.seq}`}`,
161
+ source: "wechat",
162
+ scope: isGroup ? "im_group" : "im_dm",
163
+ threadId: wechatThreadId(m),
164
+ author: { login: m.from_user_id ?? "unknown", isBot: false }, // message_type 2 already excluded
165
+ // DMs are inherently addressed; group @mention text format is not reliably parseable in
166
+ // this protocol — fail closed (allowed-list members still produce inferred).
167
+ addressing: !isGroup,
168
+ body,
169
+ ts: m.create_time_ms ? new Date(m.create_time_ms).toISOString() : new Date().toISOString(),
170
+ };
171
+ }
172
+ export class WechatSource {
173
+ opts;
174
+ kind = "wechat";
175
+ warnedNoAuth = false;
176
+ constructor(opts) {
177
+ this.opts = opts;
178
+ }
179
+ async collect(from, opts = {}) {
180
+ const auth = this.opts.store.loadAuth();
181
+ if (!auth) {
182
+ if (!this.warnedNoAuth) {
183
+ this.warnedNoAuth = true;
184
+ this.opts.log?.("[wechat] not logged in — run `wechat-login` with the same --db to arm this source");
185
+ }
186
+ return { triggers: [], next: from };
187
+ }
188
+ const client = new WechatApiClient({ token: auth.token, baseUrl: auth.baseUrl, ...(this.opts.apiBase ? { apiBase: this.opts.apiBase } : {}), ...(this.opts.fetchImpl ? { fetchImpl: this.opts.fetchImpl } : {}) });
189
+ const abort = new AbortController();
190
+ const timer = setTimeout(() => abort.abort(), this.opts.pollTimeoutMs ?? 8000);
191
+ let resp;
192
+ try {
193
+ resp = await client.getUpdates(this.opts.store.loadCursor(), abort.signal);
194
+ }
195
+ catch (e) {
196
+ if (abort.signal.aborted)
197
+ return { triggers: [], next: from }; // long-poll cut short by our own timeout — lossless, the cursor hasn't moved
198
+ throw e;
199
+ }
200
+ finally {
201
+ clearTimeout(timer);
202
+ }
203
+ if (resp.ret === 0) {
204
+ if (resp.get_updates_buf)
205
+ this.opts.store.saveCursor(resp.get_updates_buf);
206
+ const triggers = [];
207
+ for (const m of resp.msgs ?? []) {
208
+ // remember how to reply into this thread — the context_token is the routing key
209
+ if (m.from_user_id && m.context_token) {
210
+ this.opts.store.saveRoute(wechatThreadId(m), {
211
+ toUserId: m.from_user_id,
212
+ ...(m.group_id ? { groupId: m.group_id } : {}),
213
+ ...(m.session_id ? { sessionId: m.session_id } : {}),
214
+ contextToken: m.context_token,
215
+ });
216
+ }
217
+ const t = wechatTriggerFromMessage(m);
218
+ if (t && !opts.exclude?.has(t.id))
219
+ triggers.push(t);
220
+ }
221
+ return { triggers, next: from };
222
+ }
223
+ if (resp.errcode === -14 || resp.ret === -14) {
224
+ this.opts.store.clearAll();
225
+ this.opts.log?.("[wechat] session expired (-14) — state cleared, run `wechat-login` again");
226
+ return { triggers: [], next: from };
227
+ }
228
+ throw new Error(`wechat getupdates ret=${resp.ret} errcode=${resp.errcode ?? "-"}: ${resp.errmsg ?? ""}`.trim());
229
+ }
230
+ }
231
+ // ── ReplyChannel ────────────────────────────────────────────────────────────────
232
+ export class WechatReplyChannel {
233
+ store;
234
+ clientOf;
235
+ log;
236
+ constructor(store, clientOf, log = () => { }) {
237
+ this.store = store;
238
+ this.clientOf = clientOf;
239
+ this.log = log;
240
+ }
241
+ matches(threadId) {
242
+ return threadId.startsWith("wechat:");
243
+ }
244
+ async deliver(threadId, body) {
245
+ const auth = this.store.loadAuth();
246
+ const route = this.store.loadRoute(threadId);
247
+ if (!auth || !route)
248
+ throw new Error(`no wechat route for thread ${threadId} (not logged in, or the thread never sent us a message)`);
249
+ const resp = await this.clientOf(auth).sendMessage({
250
+ to_user_id: route.toUserId,
251
+ ...(route.groupId ? { group_id: route.groupId } : {}),
252
+ ...(route.sessionId ? { session_id: route.sessionId } : {}),
253
+ message_type: 2, // BOT
254
+ item_list: [{ type: 1, text_item: { text: body } }],
255
+ context_token: route.contextToken, // "回复时必须原样回传" — routes the reply
256
+ });
257
+ if (resp.ret !== 0)
258
+ throw new Error(`wechat sendmessage ret=${resp.ret}: ${resp.errmsg ?? ""}`.trim());
259
+ return { externalId: `msg/${resp.message_id ?? "unknown"}` };
260
+ }
261
+ }
262
+ //# sourceMappingURL=wechat.js.map