@vicfei/dsh-super-agent 0.6.1 → 0.8.0
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 +13 -2
- package/README.zh-CN.md +13 -2
- package/dist/cli.js +62 -9
- package/dist/cli.js.map +1 -1
- package/dist/domain.d.ts +9 -2
- package/dist/domain.js +1 -0
- package/dist/domain.js.map +1 -1
- package/dist/engine.d.ts +17 -6
- package/dist/engine.js +72 -19
- package/dist/engine.js.map +1 -1
- package/dist/feishu.d.ts +66 -0
- package/dist/feishu.js +112 -0
- package/dist/feishu.js.map +1 -0
- package/dist/github/client.d.ts +6 -0
- package/dist/github/client.js +9 -0
- package/dist/github/client.js.map +1 -1
- package/dist/github/source.d.ts +4 -2
- package/dist/github/source.js +1 -0
- package/dist/github/source.js.map +1 -1
- package/dist/github/state.d.ts +2 -4
- package/dist/github/state.js.map +1 -1
- package/dist/merge.d.ts +10 -3
- package/dist/merge.js +20 -7
- package/dist/merge.js.map +1 -1
- package/dist/panel.html +3 -1
- package/dist/slack.d.ts +57 -0
- package/dist/slack.js +106 -0
- package/dist/slack.js.map +1 -0
- package/dist/source.d.ts +25 -0
- package/dist/source.js +11 -0
- package/dist/source.js.map +1 -0
- package/dist/usage.d.ts +1 -1
- package/dist/usage.js +4 -1
- package/dist/usage.js.map +1 -1
- package/dist/watch.d.ts +5 -2
- package/dist/watch.js +12 -8
- package/dist/watch.js.map +1 -1
- package/dist/wechat.d.ts +142 -0
- package/dist/wechat.js +262 -0
- package/dist/wechat.js.map +1 -0
- package/dist/work.d.ts +14 -2
- package/dist/work.js +31 -9
- package/dist/work.js.map +1 -1
- package/examples/policy.json +57 -9
- package/package.json +4 -1
package/dist/slack.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Slack channel (v0.8 M8-3, issue #45) — Socket Mode (no public URL) bridged onto the
|
|
2
|
+
// pull-based Source contract via a capped buffer, same pattern as the Feishu channel; replies
|
|
3
|
+
// via chat.postMessage. The bot's own user id is fetched once via auth.test, so group @mention
|
|
4
|
+
// detection (<@U…> in text) is precise without manual configuration.
|
|
5
|
+
const BUFFER_CAP = 1000;
|
|
6
|
+
export function slackTriggerFromEvent(e, botUserId) {
|
|
7
|
+
if (!e.channel || !e.ts)
|
|
8
|
+
return null;
|
|
9
|
+
if (e.subtype && e.subtype !== "thread_broadcast")
|
|
10
|
+
return null; // joins/edits/deletes are not work requests
|
|
11
|
+
const isDm = e.channel_type === "im";
|
|
12
|
+
const text = e.text ?? "";
|
|
13
|
+
const addressing = isDm ? true // a DM is inherently addressed
|
|
14
|
+
: botUserId ? text.includes(`<@${botUserId}>`)
|
|
15
|
+
: false; // unknown bot id → fail closed (inferred via listedNotAddressing remains)
|
|
16
|
+
const author = e.user ?? e.bot_id ?? "unknown";
|
|
17
|
+
return {
|
|
18
|
+
id: `slack:msg:${e.channel}/${e.ts}`,
|
|
19
|
+
source: "slack",
|
|
20
|
+
scope: isDm ? "im_dm" : "im_group",
|
|
21
|
+
threadId: `slack:${isDm ? "dm" : "channel"}/${e.channel}`,
|
|
22
|
+
author: { login: author, isBot: e.bot_id !== undefined },
|
|
23
|
+
addressing,
|
|
24
|
+
body: text,
|
|
25
|
+
ts: new Date(Number(e.ts.split(".")[0]) * 1000).toISOString(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export class SlackSource {
|
|
29
|
+
opts;
|
|
30
|
+
subscribe;
|
|
31
|
+
kind = "slack";
|
|
32
|
+
buffer = [];
|
|
33
|
+
starting = null;
|
|
34
|
+
constructor(opts,
|
|
35
|
+
/** Test seam: replaces the Socket Mode subscription. */
|
|
36
|
+
subscribe) {
|
|
37
|
+
this.opts = opts;
|
|
38
|
+
this.subscribe = subscribe;
|
|
39
|
+
}
|
|
40
|
+
start() {
|
|
41
|
+
this.starting ??= this.connect();
|
|
42
|
+
return this.starting;
|
|
43
|
+
}
|
|
44
|
+
async connect() {
|
|
45
|
+
const onEvent = (e) => {
|
|
46
|
+
const t = slackTriggerFromEvent(e, this.opts.botUserId);
|
|
47
|
+
if (!t)
|
|
48
|
+
return;
|
|
49
|
+
if (this.buffer.length >= BUFFER_CAP)
|
|
50
|
+
this.buffer.shift();
|
|
51
|
+
this.buffer.push(t);
|
|
52
|
+
};
|
|
53
|
+
if (this.subscribe) {
|
|
54
|
+
await this.subscribe(onEvent);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const { SocketModeClient } = await import("@slack/socket-mode");
|
|
58
|
+
const socket = new SocketModeClient({ appToken: this.opts.appToken });
|
|
59
|
+
socket.on("message", ({ body }) => {
|
|
60
|
+
// Events API envelope: { type: "event_callback", event: {...} }
|
|
61
|
+
const inner = body.event;
|
|
62
|
+
if (inner)
|
|
63
|
+
onEvent(inner);
|
|
64
|
+
});
|
|
65
|
+
await socket.start();
|
|
66
|
+
}
|
|
67
|
+
async collect(from, opts = {}) {
|
|
68
|
+
await this.start();
|
|
69
|
+
const triggers = this.buffer.splice(0).filter((t) => !opts.exclude?.has(t.id));
|
|
70
|
+
return { triggers, next: from }; // push source: replay safety is the engine's idempotence
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export class SlackReplyChannel {
|
|
74
|
+
send;
|
|
75
|
+
constructor(send) {
|
|
76
|
+
this.send = send;
|
|
77
|
+
}
|
|
78
|
+
matches(threadId) {
|
|
79
|
+
return threadId.startsWith("slack:");
|
|
80
|
+
}
|
|
81
|
+
async deliver(threadId, body) {
|
|
82
|
+
const channel = threadId.slice(threadId.indexOf("/") + 1);
|
|
83
|
+
if (!channel || channel.includes("/"))
|
|
84
|
+
throw new Error(`cannot resolve channel from thread ${threadId}`);
|
|
85
|
+
const { ts } = await this.send(channel, body);
|
|
86
|
+
return { externalId: `msg:${channel}/${ts}` };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Build the real sender; also resolves the bot's own user id (auth.test) for @mention detection. */
|
|
90
|
+
export async function createSlackSender(botToken) {
|
|
91
|
+
const { WebClient } = await import("@slack/web-api");
|
|
92
|
+
const web = new WebClient(botToken);
|
|
93
|
+
const auth = await web.auth.test();
|
|
94
|
+
const botUserId = auth.user_id;
|
|
95
|
+
return {
|
|
96
|
+
botUserId,
|
|
97
|
+
send: async (channel, text) => {
|
|
98
|
+
const res = await web.chat.postMessage({ channel, text });
|
|
99
|
+
const ts = res.ts;
|
|
100
|
+
if (!ts)
|
|
101
|
+
throw new Error("slack postMessage returned no ts");
|
|
102
|
+
return { ts };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=slack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack.js","sourceRoot":"","sources":["../src/slack.ts"],"names":[],"mappings":"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"}
|
package/dist/source.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Trigger } from "./domain.js";
|
|
2
|
+
export interface SourceState {
|
|
3
|
+
/** Max item id already taken per feed; 0 = never polled. Feed names are source-specific. */
|
|
4
|
+
positions: Record<string, number>;
|
|
5
|
+
}
|
|
6
|
+
export interface Source {
|
|
7
|
+
/** Channel namespace — also the threadId prefix and the positions kv suffix. */
|
|
8
|
+
readonly kind: string;
|
|
9
|
+
/** Pure collection: triggers newer than `from` plus the next state; persists nothing. */
|
|
10
|
+
collect(from: SourceState, opts: {
|
|
11
|
+
exclude?: ReadonlySet<string>;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
triggers: Trigger[];
|
|
14
|
+
next: SourceState;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export interface ReplyChannel {
|
|
18
|
+
matches(threadId: string): boolean;
|
|
19
|
+
/** Deliver a reply body into the thread; returns the source-scoped external message id. */
|
|
20
|
+
deliver(threadId: string, body: string): Promise<{
|
|
21
|
+
externalId: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
/** GitHub's channel: threads are `github:owner/repo/(issues|pulls)/N`, delivery is an issue comment. */
|
|
25
|
+
export declare function threadSource(threadId: string): string;
|
package/dist/source.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Source & ReplyChannel — the multi-source contracts (v0.8, issue #45).
|
|
2
|
+
// A Source turns one channel's events into domain Triggers and owns its read state, namespaced
|
|
3
|
+
// by `kind`; the Engine persists that state in the same transaction as the triggers it produced
|
|
4
|
+
// (exactly-once across restarts — docs/001: "a Source holds its own read position").
|
|
5
|
+
// A ReplyChannel delivers outbox reply effects back where the trigger came from; routing is by
|
|
6
|
+
// threadId prefix ("github:…", "feishu:…", "slack:…", "wechat:…").
|
|
7
|
+
/** GitHub's channel: threads are `github:owner/repo/(issues|pulls)/N`, delivery is an issue comment. */
|
|
8
|
+
export function threadSource(threadId) {
|
|
9
|
+
return threadId.split(":")[0] ?? "";
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.js","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,+FAA+F;AAC/F,gGAAgG;AAChG,qFAAqF;AACrF,+FAA+F;AAC/F,mEAAmE;AAsBnE,wGAAwG;AACxG,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,OAAO,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACtC,CAAC"}
|
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)
|
|
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);\nASSESS_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)
|
|
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
|
+
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
|
|
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 {
|
|
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
|
-
|
|
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,23 +32,27 @@ export async function runWatch(opts) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
async function runCycle(opts, log) {
|
|
35
|
-
const { engine,
|
|
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
|
-
|
|
39
|
-
|
|
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,
|
|
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
|
-
log(`— cycle: fresh=${report.fresh} opened=${report.ticketsOpened} resumed=${report.resumed} worked=${reports.length} merged=${merged.length}`);
|
|
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;
|
|
53
57
|
}
|
|
54
58
|
return false;
|
package/dist/watch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;
|
|
1
|
+
{"version":3,"file":"watch.js","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"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"}
|
package/dist/wechat.d.ts
ADDED
|
@@ -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
|
+
}
|