@gavin-hu/my-pi-agent 0.18.0 → 0.20.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 +66 -52
- package/README.zh-CN.md +56 -46
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +507 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
- package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
- package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
- package/docs/extensions/file-browse.md +561 -0
- package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
- package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
- package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
- package/{extensions/job/README.md → docs/extensions/job.md} +62 -52
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +298 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +254 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
- package/extensions/board/commands.ts +105 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +170 -0
- package/extensions/board/index.ts +40 -0
- package/extensions/board/input.ts +54 -0
- package/extensions/board/join.ts +42 -0
- package/extensions/board/page.ts +388 -0
- package/extensions/board/result.ts +108 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +90 -0
- package/extensions/board/seat.ts +28 -0
- package/extensions/board/snapshot.ts +42 -0
- package/extensions/board/tools.ts +444 -0
- package/extensions/board/transport/router.ts +91 -0
- package/extensions/board/transport/server.ts +138 -0
- package/extensions/board/types.ts +121 -0
- package/extensions/board/view/client.ts +51 -0
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +125 -0
- package/extensions/board/view/live.ts +231 -0
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +118 -0
- package/extensions/board/view/render.ts +519 -0
- package/extensions/board/view/styles.ts +228 -0
- package/extensions/board/view/team.ts +327 -0
- package/extensions/bytefly/footer/budget.ts +6 -5
- package/extensions/bytefly/footer/collect/entries.ts +12 -4
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
- package/extensions/bytefly/footer/config.ts +19 -8
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +117 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- package/extensions/bytefly/panel/commands.ts +67 -7
- package/extensions/bytefly/panel/filter.ts +68 -0
- package/extensions/bytefly/panel/format.ts +16 -15
- package/extensions/bytefly/panel/join.ts +56 -13
- package/extensions/bytefly/panel/prices.ts +43 -0
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +75 -43
- package/extensions/bytefly/panel/tui/entries.ts +26 -8
- package/extensions/bytefly/panel/tui/frame.ts +130 -93
- package/extensions/bytefly/panel/tui/keys.ts +352 -0
- package/extensions/bytefly/panel/tui/list-view.ts +25 -46
- package/extensions/bytefly/panel/tui/panes.ts +76 -0
- package/extensions/bytefly/panel/tui/section-view.ts +126 -0
- package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
- package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
- package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
- package/extensions/bytefly/panel/tui.ts +306 -259
- package/extensions/bytefly/picker/discovery.ts +0 -2
- package/extensions/bytefly/picker/types.ts +0 -2
- package/extensions/bytefly/update/check.ts +39 -0
- package/extensions/bytefly/update/format.ts +15 -0
- package/extensions/bytefly/update/register.ts +73 -34
- package/extensions/bytefly/update/state.ts +6 -3
- package/extensions/check/commands.ts +2 -1
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +12 -0
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/files/tree.ts +101 -0
- package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
- package/extensions/file-browse/index.ts +4 -4
- package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
- package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
- package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
- package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
- package/extensions/file-browse/view/emit.ts +51 -0
- package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
- package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
- package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
- package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
- package/extensions/file-read/schema.ts +2 -1
- package/extensions/job/runtime.ts +4 -4
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +185 -52
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +23 -5
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +27 -34
- package/extensions/plan/policy.ts +30 -17
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +52 -11
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +20 -95
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +15 -8
- package/extensions/plan/web-blocks.ts +89 -0
- package/extensions/plan/web-chrome.ts +3 -5
- package/extensions/plan/web-client.ts +179 -330
- package/extensions/plan/web-collab.ts +3 -3
- package/extensions/plan/web-comments.ts +18 -8
- package/extensions/plan/web-composer.ts +80 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +134 -0
- package/extensions/plan/web-outline.ts +192 -0
- package/extensions/plan/web-page.ts +4 -8
- package/extensions/plan/web-plan.ts +150 -0
- package/extensions/plan/web-router.ts +3 -58
- package/extensions/plan/web-settle.ts +194 -0
- package/extensions/plan/web-share-session.ts +24 -30
- package/extensions/plan/web-streams.ts +14 -14
- package/extensions/plan/web-tokens.ts +2 -2
- package/extensions/plan/web.ts +66 -120
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
- package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
- package/extensions/rewind/{config.ts → config/config.ts} +1 -1
- package/extensions/rewind/index.ts +3 -3
- package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
- package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
- package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
- package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
- package/extensions/rewind/{format.ts → text/format.ts} +3 -3
- package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
- package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
- package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
- package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
- package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
- package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
- package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
- package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
- package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
- package/extensions/rewind/{status.ts → view/status.ts} +4 -4
- package/extensions/session-bus/commands.ts +17 -69
- package/extensions/session-bus/format.ts +22 -45
- package/extensions/session-bus/index.ts +13 -7
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +32 -4
- package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
- package/extensions/session-bus/protocol.ts +19 -49
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +16 -51
- package/extensions/session-bus/rows.ts +11 -5
- package/extensions/session-bus/runtime.ts +220 -267
- package/extensions/session-bus/store.ts +55 -7
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +5 -5
- package/extensions/session-bus/types.ts +23 -14
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +145 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +119 -0
- package/extensions/team/index.ts +34 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +267 -0
- package/extensions/team/types.ts +65 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- package/extensions/web-access/fetch/page.ts +4 -1
- package/extensions/web-access/fetch/structured.ts +447 -0
- package/extensions/web-access/fetch/tool.ts +2 -1
- package/extensions/web-access/search/tool.ts +2 -1
- package/extensions/wechat/bridge.ts +106 -251
- package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
- package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
- package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
- package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
- package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
- package/extensions/wechat/dialog/channels.ts +136 -0
- package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- package/extensions/wechat/intake.ts +245 -0
- package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
- package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
- package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
- package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
- package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
- package/extensions/wechat/{state.ts → state/state.ts} +14 -2
- package/extensions/wechat/{format.ts → text/format.ts} +3 -3
- package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
- package/extensions/wechat/tools.ts +29 -12
- package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
- package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
- package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
- package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
- package/extensions/wechat/transport/outbound.ts +142 -0
- package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
- package/extensions/wechat/types.ts +10 -1
- package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
- package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
- package/extensions/worktree/commands.ts +24 -17
- package/extensions/worktree/exit.ts +12 -6
- package/extensions/worktree/git.ts +22 -14
- package/extensions/worktree/guard.ts +18 -290
- package/extensions/worktree/handoff.ts +10 -2
- package/extensions/worktree/index.ts +2 -1
- package/extensions/worktree/lifecycle.ts +2 -0
- package/extensions/worktree/registry.ts +20 -11
- package/extensions/worktree/root-tools.ts +13 -3
- package/extensions/worktree/runtime.ts +8 -2
- package/extensions/worktree/screen.ts +15 -8
- package/extensions/worktree/shell.ts +303 -0
- package/extensions/worktree/state.ts +1 -1
- package/extensions/worktree/status.ts +42 -9
- package/extensions/worktree/tools.ts +19 -13
- package/lib/README.md +11 -8
- package/lib/board/card.ts +479 -0
- package/lib/board/links.ts +141 -0
- package/lib/board/page-pointer.ts +310 -0
- package/lib/board/round-text.ts +81 -0
- package/lib/board/round.ts +94 -0
- package/lib/board/rules.ts +437 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +497 -0
- package/lib/board/view.ts +234 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +35 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +237 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +135 -0
- package/lib/http/token.ts +38 -0
- package/lib/plan-entry.ts +11 -0
- package/lib/policy.ts +25 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +201 -0
- package/lib/session-tool.ts +184 -0
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +38 -2
- package/lib/tui/glyphs.ts +3 -2
- package/lib/web/README.md +57 -4
- package/lib/web/client-runtime.ts +123 -0
- package/lib/web/components.ts +5 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +88 -1
- package/lib/web/names.ts +42 -0
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +26 -14
- package/skills/next/SKILL.md +4 -3
- package/skills/project-docs/SKILL.md +133 -0
- package/skills/project-docs/references/audit.md +131 -0
- package/skills/project-docs/references/changelog.md +105 -0
- package/skills/project-docs/references/discovery.md +127 -0
- package/skills/project-docs/references/readme.md +80 -0
- package/skills/project-docs/references/release.md +84 -0
- package/skills/project-docs/references/roadmap.md +93 -0
- package/skills/project-docs/scripts/state.ts +316 -0
- package/skills/refactor/SKILL.md +100 -125
- package/skills/refactor/references/handoff.md +35 -0
- package/skills/refactor/references/signals.md +54 -0
- package/skills/refactor/scripts/inventory.ts +587 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/file-browse/README.md +0 -506
- package/extensions/file-browse/files.ts +0 -564
- package/extensions/file-browse/server.ts +0 -141
- package/extensions/job/process.ts +0 -35
- package/extensions/plan/split.ts +0 -87
- package/extensions/session-bus/README.md +0 -247
- package/extensions/session-bus/team.ts +0 -102
- package/extensions/wechat/outbound.ts +0 -100
- package/lib/plans/sections.ts +0 -172
- package/lib/plans/setting.ts +0 -52
- package/lib/plans/split.ts +0 -215
- package/lib/plans/store.ts +0 -19
- package/lib/plans/team.ts +0 -151
- package/skills/changelog/SKILL.md +0 -148
- package/skills/parallel-dev/SKILL.md +0 -150
- /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
- /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
- /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
- /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
- /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
- /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
- /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
- /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
- /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
- /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
- /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
|
@@ -2,68 +2,44 @@
|
|
|
2
2
|
* The WeChat bridge: one connection per account, one session per peer.
|
|
3
3
|
*
|
|
4
4
|
* This is the composition root. The poll loop, outbound delivery, the typing and
|
|
5
|
-
* wait notices, admission (dedupe/cap/budget),
|
|
6
|
-
* their own module; the bridge wires them to the live
|
|
7
|
-
* and state, and owns the connection lifecycle and the
|
|
8
|
-
* admitted message is handed to the sender's own session
|
|
9
|
-
* worker, so a WeChat turn never runs in the terminal's session.
|
|
5
|
+
* wait notices, admission (dedupe/cap/budget), inbound media, and the inbound
|
|
6
|
+
* path itself each live in their own module; the bridge wires them to the live
|
|
7
|
+
* client, account, cursor, and state, and owns the connection lifecycle and the
|
|
8
|
+
* status snapshot. Each admitted message is handed to the sender's own session
|
|
9
|
+
* through the peer worker, so a WeChat turn never runs in the terminal's session.
|
|
10
10
|
*
|
|
11
11
|
* The peer only has the chat window, so the bridge carries the signals the phone
|
|
12
12
|
* needs: typing starts as soon as a message is picked up, a message waiting on
|
|
13
13
|
* another conversation is acknowledged once and told why, a slow turn says so,
|
|
14
|
-
* and
|
|
14
|
+
* and every queued message is answered in its own turn, in order.
|
|
15
15
|
*
|
|
16
16
|
* The model may message the owner proactively through {@link Bridge.sendToOwner}.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { WechatError, type WechatClient } from "./client.ts";
|
|
20
|
-
import { createAdmissionQueue, createTurnBudget } from "./admission.ts";
|
|
21
|
-
import { createDeliverer, type MessageTarget } from "./deliver.ts";
|
|
22
|
-
import { createInboundAdmission } from "./inbound.ts";
|
|
23
|
-
import { createPollLoop, defaultPollTimeout } from "./poll.ts";
|
|
19
|
+
import { WechatError, type WechatClient } from "./transport/client.ts";
|
|
20
|
+
import { createAdmissionQueue, createTurnBudget } from "./peer/admission.ts";
|
|
21
|
+
import { createDeliverer, type MessageTarget } from "./transport/deliver.ts";
|
|
22
|
+
import { createInboundAdmission } from "./transport/inbound.ts";
|
|
23
|
+
import { createPollLoop, defaultPollTimeout } from "./transport/poll.ts";
|
|
24
24
|
import { messageOf } from "./errors.ts";
|
|
25
|
-
import {
|
|
26
|
-
import { notice } from "./notices.ts";
|
|
27
|
-
import { createPresence } from "./presence.ts";
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} from "./
|
|
25
|
+
import { peerKey, sanitizeInbound, type PeerLanguage } from "./text/format.ts";
|
|
26
|
+
import { notice } from "./text/notices.ts";
|
|
27
|
+
import { createPresence } from "./peer/presence.ts";
|
|
28
|
+
import { createForgetToken, createIntake, createLanguageOf, type Counter } from "./intake.ts";
|
|
29
|
+
|
|
30
|
+
// The admission predicates, re-exported: they are the bridge's contract, and a caller
|
|
31
|
+
// should not have to know which module inside it answers them.
|
|
32
|
+
export { isAllowedPeer, isInboundMessage } from "./intake.ts";
|
|
33
|
+
import { createPeerChannels, type PeerChannels } from "./dialog/channels.ts";
|
|
34
|
+
import type { PendingPromptInfo, WechatInteractionChannel } from "./dialog/interaction.ts";
|
|
33
35
|
import { defaultSleep, defaultTimer, type ArmTimer } from "./timer.ts";
|
|
34
|
-
import { createPeerWorker, type PeerState, type PeerWorker } from "./peer-worker.ts";
|
|
35
|
-
import type { PeerSession } from "./peer-session.ts";
|
|
36
|
-
import { emptyState,
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
findSlashCommand,
|
|
40
|
-
parseSlash,
|
|
41
|
-
SLASH_COMMANDS,
|
|
42
|
-
type SlashCommand,
|
|
43
|
-
type SlashContext,
|
|
44
|
-
} from "./slash.ts";
|
|
45
|
-
import type { BotCredentials, InboundItem, WechatState, WeixinMessage } from "./types.ts";
|
|
36
|
+
import { createPeerWorker, type PeerState, type PeerWorker } from "./peer/peer-worker.ts";
|
|
37
|
+
import type { PeerSession } from "./peer/peer-session.ts";
|
|
38
|
+
import { emptyState, ownerTarget } from "./state/state.ts";
|
|
39
|
+
import { SLASH_COMMANDS, type SlashCommand } from "./commands/slash.ts";
|
|
40
|
+
import type { BotCredentials, WechatState } from "./types.ts";
|
|
46
41
|
import type { WechatConfig } from "./config.ts";
|
|
47
42
|
|
|
48
|
-
/** Inbound messages held while a turn runs; beyond this the peer is told to wait. */
|
|
49
|
-
const MAX_QUEUE = 8;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Whether `peer` may drive the agent: the owner, or an explicit extra. Either id
|
|
53
|
-
* form is accepted (with or without `@im.wechat`), so a label copied from the
|
|
54
|
-
* control screen is not silently refused.
|
|
55
|
-
*/
|
|
56
|
-
export function isAllowedPeer(peer: string, owner: string | undefined, allowedPeers: string[]): boolean {
|
|
57
|
-
const key = peerKey(peer);
|
|
58
|
-
if (owner !== undefined && peerKey(owner) === key) return true;
|
|
59
|
-
return allowedPeers.some((allowed) => peerKey(allowed) === key);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** True when a message is inbound from a human (not the bot's own echo). */
|
|
63
|
-
export function isInboundMessage(message: WeixinMessage): message is WeixinMessage & { from_user_id: string } {
|
|
64
|
-
return message.message_type !== 2 && typeof message.from_user_id === "string" && message.from_user_id !== "";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
43
|
/** The bridge's connection state, published to the UI for the status chip. */
|
|
68
44
|
export type BridgeState = "stopped" | "running" | "expired" | "paused";
|
|
69
45
|
|
|
@@ -120,10 +96,10 @@ export interface BridgeStatus {
|
|
|
120
96
|
/** Labels of the senders refused since the bridge opened, newest last, capped. */
|
|
121
97
|
refusedPeers: string[];
|
|
122
98
|
mediaDropped: number;
|
|
123
|
-
/** Whether
|
|
99
|
+
/** Whether any peer's dialogs prompt is waiting for an answer. */
|
|
124
100
|
promptPending: boolean;
|
|
125
|
-
/**
|
|
126
|
-
|
|
101
|
+
/** Every waiting prompt, one per peer, with its kind, title, and age. */
|
|
102
|
+
prompts: PendingPromptInfo[];
|
|
127
103
|
/** When this connection opened. */
|
|
128
104
|
startedAt?: number;
|
|
129
105
|
/** When the last poll settled, successful or not. */
|
|
@@ -160,10 +136,10 @@ export interface Bridge {
|
|
|
160
136
|
/** What one peer's conversation is doing, for `/status`. */
|
|
161
137
|
peerState(peer: string): PeerState;
|
|
162
138
|
status(): BridgeStatus;
|
|
163
|
-
/**
|
|
164
|
-
channel(): WechatInteractionChannel;
|
|
165
|
-
/** Message the owner proactively
|
|
166
|
-
sendToOwner(text: string): Promise<SendResult>;
|
|
139
|
+
/** One peer's interaction channel: the channel its session routes its dialogs through. */
|
|
140
|
+
channel(peer: string): WechatInteractionChannel;
|
|
141
|
+
/** Message the owner proactively: one text, or one image, never a chosen recipient. */
|
|
142
|
+
sendToOwner(text: string | undefined, image?: string): Promise<SendResult>;
|
|
167
143
|
}
|
|
168
144
|
|
|
169
145
|
/** Create the bridge over an injected environment. */
|
|
@@ -183,7 +159,8 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
183
159
|
let client: WechatClient | undefined;
|
|
184
160
|
let account: BotCredentials | undefined;
|
|
185
161
|
let state: WechatState = { cursor: "", peers: {} };
|
|
186
|
-
|
|
162
|
+
/** Senders refused since the connection opened; the inbound path bumps it. */
|
|
163
|
+
const refused: Counter = { value: 0 };
|
|
187
164
|
/** Labels of refused senders, so the operator can allowlist one by name. */
|
|
188
165
|
const refusedPeers = new Set<string>();
|
|
189
166
|
/** The language each peer last wrote in, so a media-only message can answer. */
|
|
@@ -191,6 +168,9 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
191
168
|
/** Peers already told that the queue is full, until it drains. */
|
|
192
169
|
const overflow = new Set<string>();
|
|
193
170
|
const slashCommands = deps.slashCommands ?? SLASH_COMMANDS;
|
|
171
|
+
/** The peer's language memory: written here when a turn settles, read on every message. */
|
|
172
|
+
const languageOf = createLanguageOf(languages);
|
|
173
|
+
const forgetToken = createForgetToken({ state: () => state, saveState: deps.saveState });
|
|
194
174
|
|
|
195
175
|
/** One worker per open connection: it owns the queues and the peer sessions. */
|
|
196
176
|
let worker: PeerWorker | undefined;
|
|
@@ -216,17 +196,9 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
216
196
|
notify("WeChat session expired. Run /wechat login to reconnect.", "warning");
|
|
217
197
|
}
|
|
218
198
|
|
|
219
|
-
/** Drop a peer's cached reply token, so the next inbound message replaces it. */
|
|
220
|
-
function forgetToken(peer: string): void {
|
|
221
|
-
const entry = findPeer(state, peer);
|
|
222
|
-
if (!entry?.lastContextToken) return;
|
|
223
|
-
delete entry.lastContextToken;
|
|
224
|
-
deps.saveState(state);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
199
|
/** Send one bubble to a peer and report whether it was delivered. */
|
|
228
|
-
async function reply(target: MessageTarget, text: string): Promise<boolean> {
|
|
229
|
-
return deliverer.reply(target, text, abort?.signal);
|
|
200
|
+
async function reply(target: MessageTarget, text: string, signal?: AbortSignal): Promise<boolean> {
|
|
201
|
+
return deliverer.reply(target, text, signal ?? abort?.signal);
|
|
230
202
|
}
|
|
231
203
|
|
|
232
204
|
/** The outbound surface, reading the live client and account at call time. */
|
|
@@ -246,81 +218,31 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
246
218
|
lastSendError = undefined;
|
|
247
219
|
},
|
|
248
220
|
});
|
|
249
|
-
/** Run a slash action out-of-band and reply to the sender; never starts a turn. */
|
|
250
|
-
async function runSlashAction(command: SlashCommand, target: MessageTarget, args: string): Promise<void> {
|
|
251
|
-
if (!command.run) return;
|
|
252
|
-
const ctx: SlashContext = {
|
|
253
|
-
args,
|
|
254
|
-
peer: target.peer,
|
|
255
|
-
reply: async (text) => {
|
|
256
|
-
await reply(target, text);
|
|
257
|
-
},
|
|
258
|
-
activeTurn: worker?.state(target.peer).status === "running",
|
|
259
|
-
language: languageOf(target.peer),
|
|
260
|
-
session: {
|
|
261
|
-
newSession: () => {
|
|
262
|
-
worker?.reset(target.peer);
|
|
263
|
-
},
|
|
264
|
-
stop: () => worker?.abort(target.peer) ?? false,
|
|
265
|
-
status: () => {
|
|
266
|
-
const current = worker?.state(target.peer) ?? { status: "idle" as const, queued: 0 };
|
|
267
|
-
const waiting = interaction.pendingInfo();
|
|
268
|
-
return waiting?.peer === target.peer ? { ...current, prompt: waiting.title } : current;
|
|
269
|
-
},
|
|
270
|
-
history: (count) => deps.readHistory(target.peer, count),
|
|
271
|
-
},
|
|
272
|
-
};
|
|
273
|
-
try {
|
|
274
|
-
await command.run(ctx);
|
|
275
|
-
} catch (error) {
|
|
276
|
-
notify(`WeChat command /${command.name} failed: ${messageOf(error)}`, "warning");
|
|
277
|
-
await reply(target, `Command /${command.name} failed.`);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
221
|
/**
|
|
282
|
-
*
|
|
222
|
+
* Every peer's dialogs, one channel each.
|
|
283
223
|
*
|
|
284
|
-
* A
|
|
285
|
-
*
|
|
286
|
-
*
|
|
224
|
+
* A prompt is sent through the peer's channel to the turn it belongs to, so a
|
|
225
|
+
* reply can only answer its own peer's question even when two turns run at once.
|
|
226
|
+
* A failure is reported once per peer by the deliverer; a silent failure would
|
|
227
|
+
* leave the peer with only the choices, as if nothing preceded them.
|
|
287
228
|
*/
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const interaction = createWechatInteractionChannel({
|
|
303
|
-
send: (text, signal, options) => sendPrompt(text, signal, options),
|
|
304
|
-
sendFile: (path, name, signal) => sendFileToPeer(path, name, signal),
|
|
305
|
-
activePeer: () => worker?.current()?.peer,
|
|
306
|
-
language: () => languageOf(worker?.current()?.peer ?? ""),
|
|
229
|
+
const channels: PeerChannels = createPeerChannels({
|
|
230
|
+
flush: async (peer) => {
|
|
231
|
+
await worker?.flush(peer);
|
|
232
|
+
},
|
|
233
|
+
send: (peer, text, signal) => {
|
|
234
|
+
const item = worker?.running(peer);
|
|
235
|
+
return item ? deliverer.prompt(item, text, signal ?? abort?.signal) : Promise.resolve(false);
|
|
236
|
+
},
|
|
237
|
+
sendFile: (peer, filePath, fileName, signal) => {
|
|
238
|
+
const item = worker?.running(peer);
|
|
239
|
+
return item ? deliverer.file(item, filePath, fileName, signal ?? abort?.signal) : Promise.resolve(false);
|
|
240
|
+
},
|
|
241
|
+
active: (peer) => worker?.state(peer).status === "running",
|
|
242
|
+
language: (peer) => languageOf(peer),
|
|
307
243
|
armTimer: deps.armTimer,
|
|
308
244
|
});
|
|
309
245
|
|
|
310
|
-
/** The peer's language: from this text, else the last one they wrote. */
|
|
311
|
-
function languageOf(peer: string, text?: string): PeerLanguage {
|
|
312
|
-
// A Chinese message is evidence of Chinese. Its absence is not evidence of
|
|
313
|
-
// English, or a short reply ("ok", "1") would flip an established conversation
|
|
314
|
-
// back to English; English is the fallback, so it needs no record of its own.
|
|
315
|
-
if (text && peerLanguage(text) === "zh") languages.set(peer, "zh");
|
|
316
|
-
return languages.get(peer) ?? "en";
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/** The peer's conversation state, for the wait decisions in presence. */
|
|
320
|
-
function peerStatus(peer: string): PeerState["status"] {
|
|
321
|
-
return worker?.state(peer).status ?? "idle";
|
|
322
|
-
}
|
|
323
|
-
|
|
324
246
|
/** Typing, acknowledgement, and the slow/busy lines; one timer registry each. */
|
|
325
247
|
const presence = createPresence({
|
|
326
248
|
typingIndicator: () => deps.config.typingIndicator,
|
|
@@ -329,10 +251,10 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
329
251
|
client: () => client,
|
|
330
252
|
account: () => account,
|
|
331
253
|
signal: () => abort?.signal,
|
|
332
|
-
statusOf:
|
|
254
|
+
statusOf: (peer) => worker?.state(peer).status ?? "idle",
|
|
333
255
|
languageOf,
|
|
334
256
|
send: (peer, contextToken, text) => reply({ peer, contextToken }, text),
|
|
335
|
-
promptPending: () =>
|
|
257
|
+
promptPending: (peer) => channels.hasPending(peer),
|
|
336
258
|
});
|
|
337
259
|
|
|
338
260
|
/** Inbound media resolution and the hand-off to a peer session. */
|
|
@@ -353,92 +275,27 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
353
275
|
const budget = createTurnBudget(deps.config.maxTurnsPerHour, deps.now);
|
|
354
276
|
const admission = createAdmissionQueue({ admit: inbound.admit, notify });
|
|
355
277
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (typeof messageId === "number" && messageId !== 0 && seenMessage(state, peer, messageId)) return;
|
|
378
|
-
|
|
379
|
-
let text = rawText;
|
|
380
|
-
const cap = deps.config.maxInboundChars;
|
|
381
|
-
if (text.length > cap) {
|
|
382
|
-
const total = text.length;
|
|
383
|
-
text = `${text.slice(0, cap)}\n[WeChat message truncated: showing ${cap} of ${total} characters]`;
|
|
384
|
-
notify(`WeChat message truncated to ${cap} characters.`, "warning");
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const now = deps.now();
|
|
388
|
-
recordInbound(state, peer, message.context_token, now);
|
|
389
|
-
if (typeof messageId === "number" && messageId !== 0) rememberMessage(state, peer, messageId, now);
|
|
390
|
-
deps.saveState(state);
|
|
391
|
-
|
|
392
|
-
const target = { peer, contextToken: message.context_token ?? findPeer(state, peer)?.lastContextToken };
|
|
393
|
-
// A command is not an answer: while a dialog waits the model is blocked on it,
|
|
394
|
-
// so /stop and /status must reach their handlers or the peer's only way out is
|
|
395
|
-
// the prompt timing out. A message with media is never a command, and an
|
|
396
|
-
// unknown command falls through to the model as ordinary text.
|
|
397
|
-
const slash = media.length === 0 ? parseSlash(text) : undefined;
|
|
398
|
-
const command = slash ? findSlashCommand(slashCommands, slash.name) : undefined;
|
|
399
|
-
if (command && slash) {
|
|
400
|
-
if (command.ownerOnly && peerKey(peer) !== peerKey(owner ?? "")) {
|
|
401
|
-
refused += 1;
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
if (command.kind === "snippet") {
|
|
405
|
-
text = expandSnippet(command, slash.args);
|
|
406
|
-
} else {
|
|
407
|
-
void runSlashAction(command, target, slash.args);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
// A reply to a pending prompt is consumed here, before queueing, so it never
|
|
412
|
-
// starts a turn of its own. A duplicate answer was stopped by the id check.
|
|
413
|
-
if (interaction.handleInbound(peer, text)) return;
|
|
414
|
-
const language = languageOf(peer, text);
|
|
415
|
-
const item: InboundItem = { ...target, text, media, language };
|
|
416
|
-
if (!text && media.length === 0) {
|
|
417
|
-
// Nothing to read. A voice note without a transcript or a video is
|
|
418
|
-
// answered, so the peer knows it did not reach the agent.
|
|
419
|
-
if (unsupported > 0) void reply(item, notice("unsupported", language));
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
// A slot is reserved here, before the download, so a burst of media cannot
|
|
423
|
-
// all pass the check and then enqueue together.
|
|
424
|
-
if (!budget.allow(peer)) {
|
|
425
|
-
void reply(item, notice("budget", language));
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
const busy = (worker?.state(peer).queued ?? 0) + admission.pending(peer);
|
|
429
|
-
if (busy >= MAX_QUEUE) {
|
|
430
|
-
// Refuse rather than grow without bound; one notice per episode, not per
|
|
431
|
-
// dropped message.
|
|
432
|
-
if (!overflow.has(peer)) {
|
|
433
|
-
overflow.add(peer);
|
|
434
|
-
void reply(item, notice("overflow", language));
|
|
435
|
-
}
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
presence.keepTyping(peer, item.contextToken);
|
|
439
|
-
presence.armAck(item);
|
|
440
|
-
admission.enqueue(item);
|
|
441
|
-
}
|
|
278
|
+
/** The inbound path: admission, commands, and the hand-off to a peer's session. */
|
|
279
|
+
const intake = createIntake({
|
|
280
|
+
state: () => state,
|
|
281
|
+
saveState: deps.saveState,
|
|
282
|
+
account: () => account,
|
|
283
|
+
config: deps.config,
|
|
284
|
+
now: deps.now,
|
|
285
|
+
notify,
|
|
286
|
+
worker: () => worker,
|
|
287
|
+
channels,
|
|
288
|
+
presence,
|
|
289
|
+
budget,
|
|
290
|
+
admission,
|
|
291
|
+
languageOf,
|
|
292
|
+
reply: (target, text) => reply(target, text),
|
|
293
|
+
readHistory: deps.readHistory,
|
|
294
|
+
refused,
|
|
295
|
+
refusedPeers,
|
|
296
|
+
overflow,
|
|
297
|
+
slashCommands,
|
|
298
|
+
});
|
|
442
299
|
|
|
443
300
|
/** The long-poll loop; it reads the live client, cursor, and state. */
|
|
444
301
|
const poll = createPollLoop({
|
|
@@ -454,7 +311,7 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
454
311
|
notify,
|
|
455
312
|
onExpired: handleExpired,
|
|
456
313
|
onMessages: (messages) => {
|
|
457
|
-
for (const message of messages) handleMessage(message);
|
|
314
|
+
for (const message of messages) intake.handleMessage(message);
|
|
458
315
|
},
|
|
459
316
|
refreshLock: deps.refreshLock,
|
|
460
317
|
isRunning: () => running,
|
|
@@ -475,9 +332,9 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
475
332
|
function resetEphemeral(): void {
|
|
476
333
|
languages.clear();
|
|
477
334
|
overflow.clear();
|
|
478
|
-
refused = 0;
|
|
335
|
+
refused.value = 0;
|
|
479
336
|
refusedPeers.clear();
|
|
480
|
-
|
|
337
|
+
channels.reset();
|
|
481
338
|
deliverer.reset();
|
|
482
339
|
presence.reset();
|
|
483
340
|
budget.reset();
|
|
@@ -552,10 +409,10 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
552
409
|
// a login (or a reconnect after an expiry) starts from a clean slate while
|
|
553
410
|
// each peer's history stays on disk.
|
|
554
411
|
worker = createPeerWorker({
|
|
555
|
-
openSession: (peer, options) => deps.openSession(peer, { fresh: options.fresh, channel:
|
|
412
|
+
openSession: (peer, options) => deps.openSession(peer, { fresh: options.fresh, channel: channels.for(peer) }),
|
|
556
413
|
// Each part is delivered as its own bubble; the worker puts the
|
|
557
414
|
// attachment line on the first one. A false result stops the stream.
|
|
558
|
-
send: (item, text) => reply(item, text),
|
|
415
|
+
send: (item, text, options) => reply(item, text, options?.signal),
|
|
559
416
|
fallback: (item, reason) => notice(reason, item.language),
|
|
560
417
|
onStart: (item) => {
|
|
561
418
|
presence.cancelAck(item.peer);
|
|
@@ -602,7 +459,7 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
602
459
|
// worker, so close it unconditionally rather than leaking the sessions.
|
|
603
460
|
const closing = worker;
|
|
604
461
|
worker = undefined;
|
|
605
|
-
await closing?.close();
|
|
462
|
+
await closing?.close({ abandon: true });
|
|
606
463
|
resetEphemeral();
|
|
607
464
|
publishState();
|
|
608
465
|
releaseSafe();
|
|
@@ -618,7 +475,7 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
618
475
|
// not be cancelled by the poll abort that follows.
|
|
619
476
|
const stopping = abort;
|
|
620
477
|
abort = undefined;
|
|
621
|
-
|
|
478
|
+
await channels.cancelAll();
|
|
622
479
|
stopping?.abort();
|
|
623
480
|
try {
|
|
624
481
|
await loopPromise;
|
|
@@ -679,11 +536,11 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
679
536
|
peerCount: Object.keys(state.peers).length,
|
|
680
537
|
queued: states.reduce((total, entry) => total + entry.state.queued, 0),
|
|
681
538
|
running: states.some((entry) => entry.state.status === "running"),
|
|
682
|
-
refused,
|
|
539
|
+
refused: refused.value,
|
|
683
540
|
refusedPeers: [...refusedPeers],
|
|
684
541
|
mediaDropped: inbound.dropped(),
|
|
685
|
-
promptPending:
|
|
686
|
-
|
|
542
|
+
promptPending: channels.hasPending(),
|
|
543
|
+
prompts: channels.prompts(),
|
|
687
544
|
startedAt,
|
|
688
545
|
lastPollAt,
|
|
689
546
|
lastPollError,
|
|
@@ -694,27 +551,25 @@ export function createBridge(deps: BridgeDeps): Bridge {
|
|
|
694
551
|
};
|
|
695
552
|
},
|
|
696
553
|
|
|
697
|
-
channel(): WechatInteractionChannel {
|
|
698
|
-
return
|
|
554
|
+
channel(peer: string): WechatInteractionChannel {
|
|
555
|
+
return channels.for(peer);
|
|
699
556
|
},
|
|
700
557
|
|
|
701
|
-
async sendToOwner(text: string): Promise<SendResult> {
|
|
558
|
+
async sendToOwner(text: string | undefined, image?: string): Promise<SendResult> {
|
|
702
559
|
if (!running || !client || !account) return { ok: false, error: "WeChat bridge is not started." };
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
560
|
+
const target = ownerTarget(state, account);
|
|
561
|
+
if (text !== undefined && worker?.state(target.peer).status === "running") {
|
|
562
|
+
// The bridge answers this turn itself, so a text send would land twice.
|
|
563
|
+
return { ok: false, error: "The bridge replies to this turn automatically; do not repeat it." };
|
|
564
|
+
}
|
|
565
|
+
if (image !== undefined) {
|
|
566
|
+
// An image cannot duplicate the streamed answer, so a running turn is fine.
|
|
567
|
+
const sent = await deliverer.image(target, image, abort?.signal);
|
|
568
|
+
return sent ? { ok: true } : { ok: false, error: "The image could not be sent; see the terminal notice." };
|
|
711
569
|
}
|
|
712
|
-
const trimmed = sanitizeInbound(text);
|
|
570
|
+
const trimmed = sanitizeInbound(text ?? "");
|
|
713
571
|
if (!trimmed) return { ok: false, error: "Nothing to send." };
|
|
714
|
-
|
|
715
|
-
// and the token from an inbound `from_user_id`, and they need not agree.
|
|
716
|
-
const contextToken = findPeer(state, owner)?.lastContextToken;
|
|
717
|
-
return deliverer.toOwner(owner, contextToken, trimmed, abort?.signal);
|
|
572
|
+
return deliverer.toOwner(target.peer, target.contextToken, trimmed, abort?.signal);
|
|
718
573
|
},
|
|
719
574
|
};
|
|
720
575
|
}
|
|
@@ -18,21 +18,27 @@ import {
|
|
|
18
18
|
type ExtensionContext,
|
|
19
19
|
} from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { join } from "node:path";
|
|
21
|
-
import { writeConfigValue } from "
|
|
22
|
-
import { sanitizeLine } from "
|
|
23
|
-
import { supportsCustomUI } from "
|
|
24
|
-
import { isAllowedPeer, type Bridge } from "
|
|
25
|
-
import type { WechatClient } from "
|
|
26
|
-
import type { WechatConfig } from "
|
|
21
|
+
import { writeConfigValue } from "../../../lib/config.ts";
|
|
22
|
+
import { sanitizeLine } from "../../../lib/format.ts";
|
|
23
|
+
import { supportsCustomUI } from "../../../lib/interaction.ts";
|
|
24
|
+
import { isAllowedPeer, type Bridge } from "../bridge.ts";
|
|
25
|
+
import type { WechatClient } from "../transport/client.ts";
|
|
26
|
+
import type { WechatConfig } from "../config.ts";
|
|
27
27
|
import { doctorReport } from "./doctor.ts";
|
|
28
|
-
import { messageOf } from "
|
|
29
|
-
import { peerLabel, peerKey } from "
|
|
28
|
+
import { messageOf } from "../errors.ts";
|
|
29
|
+
import { peerLabel, peerKey } from "../text/format.ts";
|
|
30
30
|
import { waitForLogin, type LoginOutcome } from "./login.ts";
|
|
31
|
-
import { defaultSleep } from "
|
|
32
|
-
import { LoginScreenComponent, type LoginViewState } from "
|
|
33
|
-
import { qrLines } from "
|
|
34
|
-
import {
|
|
35
|
-
|
|
31
|
+
import { defaultSleep } from "../timer.ts";
|
|
32
|
+
import { LoginScreenComponent, type LoginViewState } from "../view/login-screen.ts";
|
|
33
|
+
import { qrLines } from "../../../lib/qr.ts";
|
|
34
|
+
import {
|
|
35
|
+
statusLine,
|
|
36
|
+
statusView,
|
|
37
|
+
StatusScreenComponent,
|
|
38
|
+
type StatusAction,
|
|
39
|
+
type StatusView,
|
|
40
|
+
} from "../view/status-screen.ts";
|
|
41
|
+
import type { BotCredentials } from "../types.ts";
|
|
36
42
|
|
|
37
43
|
export interface CommandDeps {
|
|
38
44
|
getBridge: () => Bridge | undefined;
|
|
@@ -152,9 +158,11 @@ function resetMessage(bridge: Bridge, peer: string): VerbOutcome {
|
|
|
152
158
|
: { message: `${label} is running; stop it before starting a new conversation.`, kind: "info" };
|
|
153
159
|
}
|
|
154
160
|
|
|
155
|
-
function cancelPromptMessage(bridge: Bridge): VerbOutcome {
|
|
156
|
-
if (!bridge.status().
|
|
157
|
-
|
|
161
|
+
function cancelPromptMessage(bridge: Bridge, peer: string): VerbOutcome {
|
|
162
|
+
if (!bridge.status().prompts.some((prompt) => prompt.peer === peer)) {
|
|
163
|
+
return { message: "No prompt is waiting.", kind: "info" };
|
|
164
|
+
}
|
|
165
|
+
void bridge.channel(peer).cancel();
|
|
158
166
|
return { message: "Cancelled the waiting prompt.", kind: "info" };
|
|
159
167
|
}
|
|
160
168
|
|
|
@@ -364,9 +372,9 @@ async function runPanel(ctx: ExtensionCommandContext, deps: CommandDeps): Promis
|
|
|
364
372
|
const bridge = deps.getBridge();
|
|
365
373
|
return bridge ? resetMessage(bridge, peer).message : "WeChat bridge is not running.";
|
|
366
374
|
},
|
|
367
|
-
cancelPrompt: () => {
|
|
375
|
+
cancelPrompt: (focused) => {
|
|
368
376
|
const bridge = deps.getBridge();
|
|
369
|
-
return bridge ? cancelPromptMessage(bridge).message : "No prompt is waiting.";
|
|
377
|
+
return bridge ? cancelPromptMessage(bridge, focused).message : "No prompt is waiting.";
|
|
370
378
|
},
|
|
371
379
|
togglePause: async () => {
|
|
372
380
|
const bridge = deps.getBridge();
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* the wording and the age math are unit-tested without a bridge.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { formatDuration } from "
|
|
11
|
-
import type { BridgeStatus } from "
|
|
12
|
-
import type { WechatConfig } from "
|
|
13
|
-
import type { BotCredentials } from "
|
|
14
|
-
import { peerLabel } from "
|
|
10
|
+
import { formatDuration } from "../../../lib/format.ts";
|
|
11
|
+
import type { BridgeStatus } from "../bridge.ts";
|
|
12
|
+
import type { WechatConfig } from "../config.ts";
|
|
13
|
+
import type { BotCredentials } from "../types.ts";
|
|
14
|
+
import { peerLabel } from "../text/format.ts";
|
|
15
15
|
|
|
16
16
|
export interface DoctorInput {
|
|
17
17
|
/** Undefined when the bridge has never been built in this runtime. */
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { SessionManager, type SessionEntry, type SessionMessageEntry } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { peerSessionDir } from "
|
|
11
|
+
import { peerSessionDir } from "../peer/peer-session.ts";
|
|
12
12
|
|
|
13
13
|
/** Longest text one history line carries before it is elided. */
|
|
14
14
|
export const HISTORY_LINE_CHARS = 160;
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* without a real timer, and the caller owns displaying the QR.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { DEFAULT_BASE_URL, type WechatClient } from "
|
|
10
|
-
import { messageOf } from "
|
|
11
|
-
import type { BotCredentials, QrResponse, QrStatus } from "
|
|
9
|
+
import { DEFAULT_BASE_URL, type WechatClient } from "../transport/client.ts";
|
|
10
|
+
import { messageOf } from "../errors.ts";
|
|
11
|
+
import type { BotCredentials, QrResponse, QrStatus } from "../types.ts";
|
|
12
12
|
|
|
13
13
|
/** Categorical meaning of a `get_qrcode_status` value. */
|
|
14
14
|
export type StatusKind = "pending" | "confirmed" | "expired" | "verify" | "blocked" | "binded";
|