@sleep2agi/agent-network 2.3.0-preview.8 → 2.3.0-preview.80
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 +35 -324
- package/dist/bin/anet.cjs +101 -0
- package/dist/bin/cli.d.ts +1 -0
- package/dist/bin/cli.js +20 -1
- package/dist/bin/goal-wake-log-render.d.ts +31 -0
- package/dist/src/active-network-task.d.ts +28 -0
- package/dist/src/batch-workdir.d.ts +9 -0
- package/dist/src/bootstrap-password-db.d.ts +13 -0
- package/dist/src/channel-attachments.d.ts +29 -0
- package/dist/src/channel-meta.d.ts +10 -0
- package/dist/src/channel-task-trace.d.ts +14 -0
- package/dist/src/claude-vendor-env.d.ts +28 -0
- package/dist/src/cli-args.d.ts +8 -0
- package/dist/src/cli-version-display.d.ts +16 -0
- package/dist/src/client-task-trace.d.ts +9 -0
- package/dist/src/client.js +1 -1
- package/dist/src/codex-copresence-preflight.d.ts +65 -0
- package/dist/src/codex-copresence-profile.d.ts +125 -0
- package/dist/src/codex-copresence-recovery.d.ts +43 -0
- package/dist/src/codex-copresence-thread.d.ts +13 -0
- package/dist/src/codex-model-default.d.ts +6 -0
- package/dist/src/codex-tui-client-health.d.ts +17 -0
- package/dist/src/commhub-response.d.ts +1 -0
- package/dist/src/controlled-upload.d.ts +83 -0
- package/dist/src/copresence-deps.d.ts +22 -0
- package/dist/src/copresence-identity.d.ts +339 -0
- package/dist/src/copresence-startup-diagnosis.d.ts +17 -0
- package/dist/src/daemon-capability-display.d.ts +110 -0
- package/dist/src/daemon-capability-display.js +21 -0
- package/dist/src/daemon-runtime-path-preflight.d.ts +22 -0
- package/dist/src/daemon-runtime-staleness.d.ts +13 -0
- package/dist/src/dashboard-managed-process.d.ts +35 -0
- package/dist/src/display-width.d.ts +27 -0
- package/dist/src/doctor-node-count.d.ts +28 -0
- package/dist/src/environ-alias.d.ts +14 -0
- package/dist/src/grok-attach-client.d.ts +129 -0
- package/dist/src/grok-build-drift.d.ts +15 -0
- package/dist/src/grok-copresence-disclosure.d.ts +11 -0
- package/dist/src/grok-copresence-orchestration.d.ts +75 -0
- package/dist/src/grok-copresence-profile.d.ts +65 -0
- package/dist/src/host-telemetry.d.ts +14 -0
- package/dist/src/hub-time-display.d.ts +20 -0
- package/dist/src/hub-version-skew.d.ts +19 -0
- package/dist/src/im/correlation-store.d.ts +34 -0
- package/dist/src/im/feishu/adapter.d.ts +87 -1
- package/dist/src/im/feishu/bridge.d.ts +85 -14
- package/dist/src/im/feishu/config.d.ts +31 -0
- package/dist/src/im/feishu/hub-upload.d.ts +88 -0
- package/dist/src/im/feishu/outbound-marker.d.ts +140 -0
- package/dist/src/im/feishu/outbound-paths.d.ts +50 -0
- package/dist/src/im/feishu/outbound-route.d.ts +62 -0
- package/dist/src/im/feishu/worker-lifecycle.d.ts +13 -0
- package/dist/src/im/feishu/worker.js +190 -139
- package/dist/src/im/types.d.ts +38 -1
- package/dist/src/local-process-state.d.ts +12 -0
- package/dist/src/locale-diagnostic.d.ts +12 -0
- package/dist/src/ls-columns.d.ts +15 -0
- package/dist/src/mock-llm.d.ts +12 -0
- package/dist/src/node-activity-log.d.ts +4 -0
- package/dist/src/node-not-found.d.ts +1 -0
- package/dist/src/node-server-payload.d.ts +66 -0
- package/dist/src/node-server.js +7 -1
- package/dist/src/normalize-runtime.d.ts +29 -2
- package/dist/src/offline-age.d.ts +29 -0
- package/dist/src/one-line-cell.d.ts +20 -0
- package/dist/src/opencode-agent-node-pair.d.ts +25 -0
- package/dist/src/opencode-auth-login.d.ts +43 -0
- package/dist/src/opencode-launch-env.d.ts +13 -0
- package/dist/src/opencode-owner-mode.d.ts +6 -0
- package/dist/src/opencode-package-binary.d.ts +21 -0
- package/dist/src/opencode-pin.d.ts +44 -0
- package/dist/src/opencode-preset.d.ts +73 -0
- package/dist/src/opencode-runtime-binding.d.ts +30 -0
- package/dist/src/opencode-safe-root.d.ts +27 -0
- package/dist/src/opencode-smoke-env.d.ts +1 -0
- package/dist/src/outbound-attachments.d.ts +57 -0
- package/dist/src/outbound-tool-names.d.ts +1 -0
- package/dist/src/owned-roots.d.ts +58 -0
- package/dist/src/owner-env-file.d.ts +2 -0
- package/dist/src/package-mode-preflight.d.ts +41 -0
- package/dist/src/posix-codex-copresence.d.ts +8 -0
- package/dist/src/posix-modes.d.ts +36 -0
- package/dist/src/primary-network.d.ts +23 -0
- package/dist/src/private-state.d.ts +10 -0
- package/dist/src/process-telemetry.d.ts +12 -0
- package/dist/src/profile-serialize.d.ts +1 -0
- package/dist/src/project-key.d.ts +1 -0
- package/dist/src/reply-originator.d.ts +20 -0
- package/dist/src/resume-runtime-infer.d.ts +18 -0
- package/dist/src/secret-shell-guidance.d.ts +3 -0
- package/dist/src/session-status-class.d.ts +48 -0
- package/dist/src/stale-socket.d.ts +118 -0
- package/dist/src/subcommand-redirect.d.ts +26 -0
- package/dist/src/task-trace.d.ts +33 -0
- package/dist/src/tmux-attach.d.ts +8 -0
- package/dist/src/tmux-capability.d.ts +58 -0
- package/dist/src/tmux-exact-target.d.ts +34 -0
- package/dist/src/tmux-pane-prompt.d.ts +22 -0
- package/dist/src/token-cli.d.ts +13 -0
- package/dist/src/tool-allowlist.d.ts +28 -0
- package/dist/src/unsafe-package-path-reason.d.ts +17 -0
- package/dist/src/win-launcher.d.ts +20 -0
- package/dist/src/windows-codex-copresence.d.ts +60 -0
- package/package.json +10 -5
|
@@ -2,22 +2,36 @@
|
|
|
2
2
|
* RFC-020 §2.5 / RFC-002 §2.2 — Feishu bridge worker.
|
|
3
3
|
*
|
|
4
4
|
* Entry point spawned by agent-node when a node profile has `channels.feishu`
|
|
5
|
-
* enabled.
|
|
6
|
-
* "agent-node direct bridge" model, not the full commhub-gateway:
|
|
5
|
+
* enabled. This worker owns the FeishuAdapter and its WSClient connection.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
* - Inbound IM event → access whitelist gate → forward to agent-node's main
|
|
10
|
-
* `think()` via parent IPC.
|
|
11
|
-
* - think() result → adapter.send() back to the originating conversation.
|
|
7
|
+
* 🔴 There are TWO outbound paths, and which one runs is decided at line ~445:
|
|
12
8
|
*
|
|
13
|
-
*
|
|
9
|
+
* const client = commhubClient ?? createEnvCommHubClient();
|
|
10
|
+
* if (client) return createCommHubEventHandler(...); // (A)
|
|
11
|
+
* if (typeof process.send === "function") ... // (B)
|
|
12
|
+
*
|
|
13
|
+
* (A) CommHub task dispatch — **the default on any real node.**
|
|
14
|
+
* `createEnvCommHubClient()` returns null only when neither COMMHUB_URL
|
|
15
|
+
* nor ANET_HUB_URL is set, and every provisioned node has one. Replies
|
|
16
|
+
* carry `in_reply_to`, and the correlation store tracks task status.
|
|
17
|
+
* (B) parent IPC → agent-node's `think()` — the fallback when there is no
|
|
18
|
+
* hub URL in env (standalone / test harness).
|
|
19
|
+
*
|
|
20
|
+
* Both paths end at adapter.send() back to the originating conversation.
|
|
21
|
+
*
|
|
22
|
+
* 🔴 This block used to say the opposite —— verbatim:
|
|
23
|
+
* "IM messages do NOT pass through commhub task dispatch."
|
|
24
|
+
* That was true of the 2026-06-24 first cut, and stayed in the file after
|
|
25
|
+
* the commhub path landed (#1252, merged 2026-08-27). It is the first thing
|
|
26
|
+
* anyone reads in this file, so it mis-answered the question "does Feishu go
|
|
27
|
+
* through CommHub" for at least one reader before being caught. If you change
|
|
28
|
+
* which path is default, change these lines in the same commit.
|
|
29
|
+
*
|
|
30
|
+
* Still true of both paths (unchanged from the first cut):
|
|
14
31
|
* - No separate gateway ntok_ / dedicated commhub alias.
|
|
15
|
-
* - IM messages do NOT pass through commhub task dispatch.
|
|
16
|
-
* - Feishu messages do NOT appear in Dashboard topology / Chat.
|
|
17
32
|
*
|
|
18
|
-
* The
|
|
19
|
-
*
|
|
20
|
-
* — tracked in #182.
|
|
33
|
+
* The remaining §2.9 work (meta_json columns, SSE passthrough, Dashboard
|
|
34
|
+
* topology / Chat visibility for Feishu conversations) is tracked in #182.
|
|
21
35
|
*
|
|
22
36
|
* Milestones:
|
|
23
37
|
* M1: worker entry scaffold.
|
|
@@ -29,8 +43,29 @@
|
|
|
29
43
|
* M4: agent-node spawn integration (fork(this) wired by agent-node).
|
|
30
44
|
* M5: group @bot trigger refinement, image up/down, Docker smoke.
|
|
31
45
|
*/
|
|
32
|
-
import type { NormalizedIMEvent } from "../types.js";
|
|
46
|
+
import type { IMCorrelationStore, NormalizedIMEvent } from "../types.js";
|
|
33
47
|
import { FeishuAdapter } from "./adapter.js";
|
|
48
|
+
export type IMBridgeCommHubInboxMessage = {
|
|
49
|
+
id: string;
|
|
50
|
+
type?: string;
|
|
51
|
+
content: string;
|
|
52
|
+
from_session?: string;
|
|
53
|
+
in_reply_to?: string;
|
|
54
|
+
meta?: unknown;
|
|
55
|
+
};
|
|
56
|
+
export interface IMBridgeCommHubClient {
|
|
57
|
+
sendTask(args: {
|
|
58
|
+
alias: string;
|
|
59
|
+
task: string;
|
|
60
|
+
priority?: "high" | "normal" | "low";
|
|
61
|
+
ttlSeconds?: number;
|
|
62
|
+
meta?: unknown;
|
|
63
|
+
}): Promise<{
|
|
64
|
+
taskId: string;
|
|
65
|
+
}>;
|
|
66
|
+
getInbox(alias: string): Promise<IMBridgeCommHubInboxMessage[]>;
|
|
67
|
+
ackInbox(alias: string, messageId: string): Promise<void>;
|
|
68
|
+
}
|
|
34
69
|
export interface FeishuBridgeOptions {
|
|
35
70
|
/** Absolute path to `.anet/nodes/<node>/channels/feishu/`. */
|
|
36
71
|
channelDir: string;
|
|
@@ -44,11 +79,28 @@ export interface FeishuBridgeOptions {
|
|
|
44
79
|
* - stderr logger otherwise (standalone smoke debugging).
|
|
45
80
|
*/
|
|
46
81
|
onEvent?: (event: NormalizedIMEvent) => Promise<void>;
|
|
82
|
+
/** Fatal WS failure after initial readiness (for worker lifecycle ownership). */
|
|
83
|
+
onTerminalError?: (error: Error) => void;
|
|
84
|
+
/** Persistent task/message correlation state. Defaults to channelDir/state.json. */
|
|
85
|
+
correlationStore?: IMCorrelationStore;
|
|
86
|
+
/** CommHub task transport. Defaults to COMMHUB_URL/COMMHUB_TOKEN when available. */
|
|
87
|
+
commhubClient?: IMBridgeCommHubClient;
|
|
88
|
+
/** Poll interval for CommHub replies. Defaults to 1500ms. */
|
|
89
|
+
commhubPollMs?: number;
|
|
90
|
+
/** 出站传输模式。省略时按 ANET_FEISHU_BRIDGE_MODE,再省略则 "direct"。 */
|
|
91
|
+
bridgeMode?: FeishuBridgeMode;
|
|
47
92
|
}
|
|
48
93
|
/** Bridge → parent: inbound IM event ready for think(). */
|
|
49
94
|
export interface BridgeIncomingEnvelope {
|
|
50
95
|
type: "event";
|
|
51
96
|
event: NormalizedIMEvent;
|
|
97
|
+
/** Canonical outbound directory for this conversation (RFC-020 §15.1).
|
|
98
|
+
* Single source of truth — the agent-node injects this verbatim into
|
|
99
|
+
* the system prompt's "save files here" instruction, and the bridge
|
|
100
|
+
* whitelist accepts files only under this directory. Computed by the
|
|
101
|
+
* bridge from `event.conversation.conversationId` + `adapter
|
|
102
|
+
* .connectionName`. Trailing slash included. */
|
|
103
|
+
outboundDir?: string;
|
|
52
104
|
}
|
|
53
105
|
/** Parent → bridge: agent reply text for a previously-forwarded event. */
|
|
54
106
|
export interface BridgeReplyEnvelope {
|
|
@@ -94,6 +146,24 @@ export type WithRateLimitHandle = {
|
|
|
94
146
|
floodKeyCount: number;
|
|
95
147
|
};
|
|
96
148
|
};
|
|
149
|
+
/**
|
|
150
|
+
* 出站传输模式。**显式声明,不靠环境变量在场与否去猜。**
|
|
151
|
+
*
|
|
152
|
+
* direct — parent IPC → agent-node 的 think()。**默认。**
|
|
153
|
+
* 2026-06-24 的第一版路径,飞书消息不进 CommHub 任务分发,
|
|
154
|
+
* 也不出现在 Dashboard 拓扑 / Chat 里。
|
|
155
|
+
* commhub — 入站事件变成一个 CommHub task,回复带 in_reply_to,
|
|
156
|
+
* correlation store 跟踪状态。**必须显式打开。**
|
|
157
|
+
*
|
|
158
|
+
* 🔴 为什么要显式:在此之前是 `commhubClient ?? createEnvCommHubClient()` ——
|
|
159
|
+
* 有 COMMHUB_URL 就走 commhub、没有就悄悄回落 IPC。两条路径的可观测性、
|
|
160
|
+
* Dashboard 可见性、失败语义完全不同,而**运维看不出自己在哪条上**,
|
|
161
|
+
* 文件头注释也因此和代码说了相反的话长达一次发布。
|
|
162
|
+
*/
|
|
163
|
+
export type FeishuBridgeMode = "commhub" | "direct";
|
|
164
|
+
export declare const DEFAULT_FEISHU_BRIDGE_MODE: FeishuBridgeMode;
|
|
165
|
+
/** 解析模式。优先级:显式入参 > 环境变量 > 默认。非法值直接抛,不静默取默认。 */
|
|
166
|
+
export declare function resolveFeishuBridgeMode(explicit?: FeishuBridgeMode, env?: NodeJS.ProcessEnv): FeishuBridgeMode;
|
|
97
167
|
/**
|
|
98
168
|
* IPC handler — forwards inbound events to the parent agent-node and routes
|
|
99
169
|
* the parent's reply back to Feishu via the adapter. The parent contract is
|
|
@@ -118,7 +188,8 @@ export type WithRateLimitHandle = {
|
|
|
118
188
|
* the agent-node.log (closes the prior silent-success blindspot).
|
|
119
189
|
*/
|
|
120
190
|
/** @internal exported for test harness; not intended for production callers. */
|
|
121
|
-
export declare function createIPCEventHandler(adapter: FeishuAdapter, ttlMs: number, ackPlaceholder: boolean): (event: NormalizedIMEvent) => Promise<void>;
|
|
191
|
+
export declare function createIPCEventHandler(adapter: FeishuAdapter, ttlMs: number, ackPlaceholder: boolean, correlationStore?: IMCorrelationStore): (event: NormalizedIMEvent) => Promise<void>;
|
|
192
|
+
export declare function createCommHubEventHandler(nodeAlias: string, adapter: FeishuAdapter, ttlMs: number, ackPlaceholder: boolean, correlationStore: IMCorrelationStore | undefined, commhubClient: IMBridgeCommHubClient, pollMs?: number): (event: NormalizedIMEvent) => Promise<void>;
|
|
122
193
|
export { FeishuAdapter } from "./adapter.js";
|
|
123
194
|
export { loadFeishuChannelConfig } from "./config.js";
|
|
124
195
|
export type { FeishuAccessList, FeishuChannelConfig, FeishuChannelEnv, } from "./config.js";
|
|
@@ -8,6 +8,31 @@ export interface FeishuAccessList {
|
|
|
8
8
|
/** Feishu chat_ids the bot is permitted to listen in. */
|
|
9
9
|
allowChats: string[];
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Outbound text-reply rendering mode (RFC-020 §16). Controls how
|
|
13
|
+
* `adapter.send` handles a text/markdown payload that doesn't already
|
|
14
|
+
* carry an `imagePath` / `files[]` (those upload routes are unaffected).
|
|
15
|
+
*
|
|
16
|
+
* - "plain" (DEFAULT): always send `msg_type:text`. Bot replies are
|
|
17
|
+
* fully copy-pasteable in Feishu; long replies are chunked into
|
|
18
|
+
* multiple text messages instead of being PNG-rendered. This is the
|
|
19
|
+
* correct default for issue/code/CLI bot replies where users want
|
|
20
|
+
* to grab the text. Vincent 2026-06-30 explicit ask: "issue 发文字".
|
|
21
|
+
*
|
|
22
|
+
* - "card": short markdown (bold, list, link, inline code) goes via
|
|
23
|
+
* schema 1.0 interactive card with `markdown` element — text stays
|
|
24
|
+
* copy-friendly + gets bolds/bullets styled. Heading/table/long
|
|
25
|
+
* fall back to plain text (no PNG). Suited to operators who want
|
|
26
|
+
* light formatting without losing copy.
|
|
27
|
+
*
|
|
28
|
+
* - "auto": preserve the pre-2026-06-30 behavior — markdown with
|
|
29
|
+
* headings / tables / >2000 chars is rendered to PNG via headless
|
|
30
|
+
* chromium (#329 path), short markdown goes to schema 1.0 card,
|
|
31
|
+
* plain text goes to msg_type:text. Highest fidelity at the cost
|
|
32
|
+
* of copy-paste. Opt-in for operators who genuinely need rendered
|
|
33
|
+
* tables / heading hierarchy in chat.
|
|
34
|
+
*/
|
|
35
|
+
export type OutboundRenderMode = "plain" | "card" | "auto";
|
|
11
36
|
export interface FeishuChannelConfig {
|
|
12
37
|
appId: string;
|
|
13
38
|
appSecret: string;
|
|
@@ -20,6 +45,12 @@ export interface FeishuChannelConfig {
|
|
|
20
45
|
auditRaw: boolean;
|
|
21
46
|
/** Per-task timeout in ms; default 5 min (RFC-020 §4.5). */
|
|
22
47
|
taskTimeoutMs: number;
|
|
48
|
+
/**
|
|
49
|
+
* RFC-020 §16 outbound rendering mode for text replies. Default `"plain"`.
|
|
50
|
+
* See `OutboundRenderMode` for per-mode semantics. Channels that omit
|
|
51
|
+
* the field get `"plain"` — Vincent's "issue 发文字" default.
|
|
52
|
+
*/
|
|
53
|
+
outboundRender: OutboundRenderMode;
|
|
23
54
|
/**
|
|
24
55
|
* Absolute path to the channel directory. The adapter writes downloaded
|
|
25
56
|
* inbound media to `<channelDir>/media/` (M5c). Populated by the loader so
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-020 §17 — feishu inbound bridge → hub `/api/upload` integration.
|
|
3
|
+
*
|
|
4
|
+
* Vincent 2026-06-30: cross-machine agents (e.g. TM门户运维@toodadev2)
|
|
5
|
+
* delegated by the feishu-local bot can't read inbound attachments
|
|
6
|
+
* because the only thing they get is a host-local path that doesn't
|
|
7
|
+
* exist on the receiver's filesystem. #351 already wired agent-node's
|
|
8
|
+
* receiver to prefer `file_id` and pull via `GET /api/files/<id>` when
|
|
9
|
+
* present. This module is the SENDER half: the feishu bridge uploads
|
|
10
|
+
* each downloaded inbound file to the hub via `POST /api/upload`,
|
|
11
|
+
* gets back a `file_id`, and propagates it alongside the local path.
|
|
12
|
+
*
|
|
13
|
+
* Failure mode (load-bearing): every failure path returns `null` and
|
|
14
|
+
* the caller falls back to path-only. The bridge MUST NOT crash because
|
|
15
|
+
* the hub is down / overloaded / refused a 12 MiB cap. Single-host
|
|
16
|
+
* `agent-node` Read still works on the local path; only cross-host
|
|
17
|
+
* delegation degrades, no regression vs the pre-fix baseline.
|
|
18
|
+
*
|
|
19
|
+
* Concurrency cap: a single feishu message can carry many images; bursts
|
|
20
|
+
* to /api/upload would hammer the hub rate limit (60/hour per token).
|
|
21
|
+
* `uploadFilesToHubConcurrent` caps in-flight requests at `concurrency`
|
|
22
|
+
* (default 4) so a 20-image message takes 5 sequential rounds instead
|
|
23
|
+
* of 20 simultaneous open sockets + a likely rate-limit denial.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Hub upload limit, mirrors `server/src/uploads.ts:MAX_UPLOAD_BYTES`.
|
|
27
|
+
* Anything bigger is skipped at the bridge — the hub would 413 anyway.
|
|
28
|
+
*/
|
|
29
|
+
export declare const HUB_UPLOAD_LIMIT_BYTES: number;
|
|
30
|
+
/** Default in-flight cap for `uploadFilesToHubConcurrent`. */
|
|
31
|
+
export declare const DEFAULT_UPLOAD_CONCURRENCY = 4;
|
|
32
|
+
/**
|
|
33
|
+
* One file uploaded to the hub. `mime` and `size` are from the local
|
|
34
|
+
* filesystem read; `file_id` and `path` come from the hub's response.
|
|
35
|
+
*
|
|
36
|
+
* `file_id` is the canonical cross-machine handle — receiving agent
|
|
37
|
+
* resolves to bytes via `GET /api/files/<file_id>`. `path` is the
|
|
38
|
+
* hub-machine-local absolute path the hub stored to; on the sender
|
|
39
|
+
* side it's purely informational (we already have the source file).
|
|
40
|
+
*/
|
|
41
|
+
export interface HubUploadResult {
|
|
42
|
+
file_id: string;
|
|
43
|
+
path?: string;
|
|
44
|
+
mime?: string;
|
|
45
|
+
size?: number;
|
|
46
|
+
name?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface HubUploadOpts {
|
|
49
|
+
/** Hub base URL, no trailing slash. Defaults to env. */
|
|
50
|
+
hubUrl: string;
|
|
51
|
+
/** Bearer token (ntok_ / utok_ / atok_). Defaults to env. */
|
|
52
|
+
authToken: string;
|
|
53
|
+
/** Optional MIME hint; sniffed by filename ext otherwise. */
|
|
54
|
+
mime?: string;
|
|
55
|
+
/** Optional override filename (defaults to basename(path)). */
|
|
56
|
+
name?: string;
|
|
57
|
+
/** Injectable for tests. Defaults to global fetch. */
|
|
58
|
+
fetch?: typeof fetch;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Upload one local file to `${hubUrl}/api/upload`. Returns the file
|
|
62
|
+
* descriptor on success, or `null` on any failure (caller falls back
|
|
63
|
+
* to path-only).
|
|
64
|
+
*
|
|
65
|
+
* - File must exist + be ≤ 12 MiB. Larger → skipped silently with
|
|
66
|
+
* a stderr warn; caller gets `null` and ships path-only.
|
|
67
|
+
* - Auth via `Authorization: Bearer <token>`.
|
|
68
|
+
* - `Content-Length` header is set by `fetch` when given a Blob body;
|
|
69
|
+
* the hub validates it before consuming bytes.
|
|
70
|
+
* - Multipart body with a single `file` field. The hub server
|
|
71
|
+
* reads `form.get("file")` and stores under a new file_id.
|
|
72
|
+
* - Any network error / non-2xx / malformed JSON response → null.
|
|
73
|
+
*/
|
|
74
|
+
export declare function uploadToHub(filePath: string, opts: HubUploadOpts): Promise<HubUploadResult | null>;
|
|
75
|
+
/**
|
|
76
|
+
* Upload many files to the hub with a bounded concurrency window.
|
|
77
|
+
* Returns one result per input path, preserving order. A failed
|
|
78
|
+
* upload appears as `null` at that slot — caller composes path-only
|
|
79
|
+
* descriptors for those.
|
|
80
|
+
*
|
|
81
|
+
* Why this matters: one feishu post can carry an arbitrary number of
|
|
82
|
+
* images. Naive `Promise.all(paths.map(uploadToHub))` would open N
|
|
83
|
+
* simultaneous HTTPS sockets to the hub AND blow past the 60/hour
|
|
84
|
+
* per-token rate limit on a single message with >60 images. A
|
|
85
|
+
* concurrency cap of 4 means N/4 sequential rounds, predictable
|
|
86
|
+
* load, no head-of-line blocking on the agent's reply.
|
|
87
|
+
*/
|
|
88
|
+
export declare function uploadFilesToHubConcurrent(filePaths: string[], opts: HubUploadOpts, concurrency?: number): Promise<(HubUploadResult | null)[]>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-020 §15 — agent→bridge outbound file protocol.
|
|
3
|
+
*
|
|
4
|
+
* 2026-06-29 Vincent UAT: agent generated a PDF via reportlab + wrote it
|
|
5
|
+
* to `/work/feishu-attachments/<conv>/x.pdf`, then in text asked Vincent
|
|
6
|
+
* "I'm not sure if the system will auto-attach this — try another way?"
|
|
7
|
+
* The bridge has `adapter.send({files})` since #329, but no protocol
|
|
8
|
+
* told the agent how to signal "dispatch this file to the user".
|
|
9
|
+
*
|
|
10
|
+
* Convention: in its final reply, the agent emits one
|
|
11
|
+
*
|
|
12
|
+
* [[send-file:/abs/path]]
|
|
13
|
+
*
|
|
14
|
+
* marker per file (one per line, end of reply). The bridge:
|
|
15
|
+
*
|
|
16
|
+
* 1. extracts every `[[send-file:...]]` token,
|
|
17
|
+
* 2. strips them from the user-visible text,
|
|
18
|
+
* 3. validates each path against an allowed-outbound-root whitelist
|
|
19
|
+
* (`/work/feishu-attachments/<conn>/<chat>/` — same per-conversation
|
|
20
|
+
* directory the LLM has Write access to under Layer B),
|
|
21
|
+
* 4. checks the file exists, is non-empty, and ≤ 30 MB (Feishu file_v1
|
|
22
|
+
* upper bound),
|
|
23
|
+
* 5. magic-byte sniffs to choose `image_key` vs `file_key` upload route,
|
|
24
|
+
* 6. dispatches each file via `adapter.send({files:[...]})`,
|
|
25
|
+
* 7. emits a friendly fallback if validation fails — never the raw error.
|
|
26
|
+
*
|
|
27
|
+
* Pure module — no I/O, no IM SDK calls. The parser returns the cleaned
|
|
28
|
+
* text + a structured list of file requests; bridge.ts is responsible
|
|
29
|
+
* for filesystem checks and dispatching.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* One outbound-file request parsed from an agent reply.
|
|
33
|
+
*
|
|
34
|
+
* - `raw`: the bytes between `[[send-file:` and `]]`, before any
|
|
35
|
+
* normalization. Kept for stderr forensic.
|
|
36
|
+
* - `normalized`: lexical-normalized absolute path (quote-strip, `..`
|
|
37
|
+
* collapse, `//` collapse, `./` resolution against cwd). The
|
|
38
|
+
* `validatedAbsolute` field is set later by the bridge after the
|
|
39
|
+
* realpath / whitelist / exists check.
|
|
40
|
+
*/
|
|
41
|
+
export interface OutboundFileRequest {
|
|
42
|
+
raw: string;
|
|
43
|
+
normalized: string;
|
|
44
|
+
}
|
|
45
|
+
export interface MarkerParseResult {
|
|
46
|
+
/** Reply text with all `[[send-file:...]]` markers removed + adjacent
|
|
47
|
+
* whitespace collapsed. May be empty (file-only reply). */
|
|
48
|
+
cleanedText: string;
|
|
49
|
+
/** Files in source order. Caller-side validation happens after. */
|
|
50
|
+
files: OutboundFileRequest[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Lexically normalize a path token from the marker. NOT a full
|
|
54
|
+
* symlink/realpath resolver — bridge does that after this returns.
|
|
55
|
+
*
|
|
56
|
+
* - Trim leading / trailing whitespace.
|
|
57
|
+
* - Strip one layer of matching quotes (`"/x"` / `'/x'` → `/x`).
|
|
58
|
+
* - `.` and `..` segments collapse via `path.posix.normalize`.
|
|
59
|
+
* - Repeated `/` collapse.
|
|
60
|
+
*
|
|
61
|
+
* Returns an absolute path or the empty string if the input cannot be
|
|
62
|
+
* interpreted as an absolute path token (caller drops empties).
|
|
63
|
+
*/
|
|
64
|
+
export declare function normalizeMarkerPath(raw: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Parse an agent reply into cleaned text + outbound-file requests.
|
|
67
|
+
*
|
|
68
|
+
* Empty / null input returns `{cleanedText: "", files: []}` so callers
|
|
69
|
+
* can safely pipe through without defensive checks.
|
|
70
|
+
*
|
|
71
|
+
* Marker-only reply (no surrounding text) returns `cleanedText: ""` —
|
|
72
|
+
* caller should NOT send an empty text message in that case; they only
|
|
73
|
+
* dispatch the files.
|
|
74
|
+
*
|
|
75
|
+
* Multiple markers on one reply are all captured. Adjacent whitespace
|
|
76
|
+
* around stripped markers is collapsed (no double blank lines).
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseOutboundMarkers(reply: string | null | undefined): MarkerParseResult;
|
|
79
|
+
export { OUTBOUND_ROOT, feishuConvKey, feishuOutboundDir } from "./outbound-paths.js";
|
|
80
|
+
/**
|
|
81
|
+
* Validate a normalized absolute path against the per-conversation
|
|
82
|
+
* whitelist + size + existence checks. Returns null on success or a
|
|
83
|
+
* Chinese user-friendly reason on failure (the same string can be
|
|
84
|
+
* surfaced as a fallback text reply when ALL files fail validation).
|
|
85
|
+
*
|
|
86
|
+
* Filesystem-touching — bridge calls this AFTER `parseOutboundMarkers`.
|
|
87
|
+
*
|
|
88
|
+
* Two API shapes accepted (overload):
|
|
89
|
+
*
|
|
90
|
+
* 1. **Preferred**: caller passes `expectedDir` — the precomputed
|
|
91
|
+
* `/work/feishu-attachments/<connectionName>/<convKey>/` value
|
|
92
|
+
* from `feishuOutboundDir`. This guarantees what the agent was
|
|
93
|
+
* TOLD to use (via cli.ts prompt injection) matches what we
|
|
94
|
+
* ACCEPT here — no second algorithm.
|
|
95
|
+
*
|
|
96
|
+
* 2. **Legacy**: caller passes `connectionName` + `convKey`. We
|
|
97
|
+
* compute the prefix here via the same helper. Kept for the
|
|
98
|
+
* single-arg unit-test ergonomics + backwards-compat (the old
|
|
99
|
+
* bridge wire-up shape).
|
|
100
|
+
*
|
|
101
|
+
* Params:
|
|
102
|
+
* - p: normalized absolute path candidate
|
|
103
|
+
* - expectedDir: precomputed whitelist directory (trailing `/`)
|
|
104
|
+
* OR
|
|
105
|
+
* - convKey + connectionName: legacy two-piece form
|
|
106
|
+
* - statFn: optional override for tests (defaults to fs.statSync)
|
|
107
|
+
* - realpathFn: optional override for tests (defaults to fs.realpathSync)
|
|
108
|
+
*
|
|
109
|
+
* The whitelist check uses literal `.startsWith(expectedDir)` — the
|
|
110
|
+
* trailing slash on `expectedDir` ensures `oc_abc/` doesn't accept
|
|
111
|
+
* paths under `oc_abc_evil/`.
|
|
112
|
+
*/
|
|
113
|
+
export interface ValidateOpts {
|
|
114
|
+
p: string;
|
|
115
|
+
/** Precomputed prefix (with trailing `/`). Preferred over convKey+connectionName. */
|
|
116
|
+
expectedDir?: string;
|
|
117
|
+
convKey?: string;
|
|
118
|
+
connectionName?: string;
|
|
119
|
+
statFn?: (p: string) => {
|
|
120
|
+
size: number;
|
|
121
|
+
};
|
|
122
|
+
realpathFn?: (p: string) => string;
|
|
123
|
+
}
|
|
124
|
+
export declare function validateOutboundPath(opts: ValidateOpts): string | null;
|
|
125
|
+
/**
|
|
126
|
+
* Magic-byte sniffer — picks `image` vs `file` upload route. Reads the
|
|
127
|
+
* first 16 bytes only; safe to call before commitment. Returns:
|
|
128
|
+
*
|
|
129
|
+
* - "image" when the bytes are a PNG / JPG / GIF / WebP / BMP signature
|
|
130
|
+
* (Feishu image_v1 supports these natively, with chat inline render).
|
|
131
|
+
* - "file" otherwise (PDF, TXT, audio, video, archive, …).
|
|
132
|
+
*
|
|
133
|
+
* Caller supplies a Buffer (bridge reads via fs.openSync + readSync).
|
|
134
|
+
*/
|
|
135
|
+
export declare function sniffFileKind(buf: Buffer): "image" | "file";
|
|
136
|
+
/**
|
|
137
|
+
* User-facing fallback message when ALL marker validations fail and the
|
|
138
|
+
* stripped text is empty (would leave the user with no reply at all).
|
|
139
|
+
*/
|
|
140
|
+
export declare const ALL_FILES_FAILED_FALLBACK = "[\u6587\u4EF6\u9644\u4EF6\u53D1\u9001\u5931\u8D25] \u6211\u51C6\u5907\u597D\u4E86\u6587\u4EF6\u4F46\u5206\u53D1\u65F6\u51FA\u4E86\u95EE\u9898\u2014\u2014\u7A0D\u540E\u518D\u8BD5\u6216\u6362\u79CD\u65B9\u5F0F\u3002";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-020 §15.1 — feishu outbound/attachment path helpers (SHARED).
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for `/work/feishu-attachments/<conn>/<convKey>/`
|
|
5
|
+
* naming. Used by:
|
|
6
|
+
*
|
|
7
|
+
* - `adapter.ts` inbound `downloadImage` (subdir for received images),
|
|
8
|
+
* - `bridge.ts` outbound marker whitelist (where the agent may write),
|
|
9
|
+
* - `worker.ts` outbound envelope (the concrete path injected into the
|
|
10
|
+
* agent's prompt, so what we tell the agent matches what we accept).
|
|
11
|
+
*
|
|
12
|
+
* 2026-06-29 Vincent UAT: prior to this module, the inbound downloader
|
|
13
|
+
* used `conversationId.replace(/[^a-zA-Z0-9_-]/g, "_")` and the outbound
|
|
14
|
+
* bridge whitelist used `sender.id` for DMs / `chatId` for groups. The
|
|
15
|
+
* two algorithms diverged — agent learned the inbound oc_<chat>/ layout
|
|
16
|
+
* from incoming attachments, then wrote outbound files to the same
|
|
17
|
+
* oc_<chat>/ subdir, but the bridge required ou_<userid>/. Every PDF
|
|
18
|
+
* generated by the bot was rejected. This module unifies on
|
|
19
|
+
* `conversationId` (the open_chat_id assigned by Feishu — present for
|
|
20
|
+
* DMs and groups equally).
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Sanitize a raw Feishu conversation id (`oc_<hex>` / occasionally
|
|
24
|
+
* `om_<id>` for DMs) into a filesystem-safe segment. Identical to the
|
|
25
|
+
* adapter.ts inbound download path policy — DO NOT CHANGE without also
|
|
26
|
+
* migrating any on-disk attachments under the old key.
|
|
27
|
+
*
|
|
28
|
+
* The regex strips anything outside `[A-Za-z0-9_-]`, replacing with `_`.
|
|
29
|
+
* Falsy / non-string input returns the empty string; caller is
|
|
30
|
+
* responsible for substituting a fallback (the empty key would resolve
|
|
31
|
+
* to `/work/feishu-attachments/<conn>//` which the path-prefix
|
|
32
|
+
* whitelist rejects cleanly).
|
|
33
|
+
*/
|
|
34
|
+
export declare function feishuConvKey(rawId: string | undefined | null): string;
|
|
35
|
+
/**
|
|
36
|
+
* Compute the canonical per-conversation directory for outbound file
|
|
37
|
+
* dispatch + inbound attachment downloads. Trailing slash included so
|
|
38
|
+
* `.startsWith(...)` whitelist checks are unambiguous about the
|
|
39
|
+
* conversation boundary (no `oc_abc` matching `oc_abc_x`).
|
|
40
|
+
*
|
|
41
|
+
* Example:
|
|
42
|
+
* feishuOutboundDir("feishu-local", "oc_9a7eedbba275a87999f7ee2cfb10f4cb")
|
|
43
|
+
* → "/work/feishu-attachments/feishu-local/oc_9a7eedbba275a87999f7ee2cfb10f4cb/"
|
|
44
|
+
*/
|
|
45
|
+
export declare function feishuOutboundDir(connectionName: string, rawConvId: string | undefined | null): string;
|
|
46
|
+
/**
|
|
47
|
+
* The fixed prefix root used as a sanity check by the marker validator.
|
|
48
|
+
* Useful for tests + for `validateOutboundPath` defense-in-depth.
|
|
49
|
+
*/
|
|
50
|
+
export declare const OUTBOUND_ROOT = "/work/feishu-attachments";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-020 §16.1 — feishu outbound route decision (pure helper).
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the `adapter.send` decision tree that
|
|
5
|
+
* picks among text / interactive card / image (rendered PNG) / file /
|
|
6
|
+
* imagePath outputs. Extracted into a pure module so the production
|
|
7
|
+
* call site AND the unit tests can bind the same function — previously
|
|
8
|
+
* the test mirrored the decision tree in a sibling `pickRoute` helper,
|
|
9
|
+
* which would drift silently if the production tree changed.
|
|
10
|
+
*
|
|
11
|
+
* Inputs are the minimal data the decision needs:
|
|
12
|
+
*
|
|
13
|
+
* - `text` / `imagePath` / `files` — payload from `NormalizedIMMessage`
|
|
14
|
+
* - `forceTextOnly` — caption-mode hint (RFC-020 §15.2)
|
|
15
|
+
* - `mode` — channel-config `outboundRender` (`"plain"` / `"card"` /
|
|
16
|
+
* `"auto"`, RFC-020 §16)
|
|
17
|
+
*
|
|
18
|
+
* Outputs the chosen `route` plus a short `reason` string for log /
|
|
19
|
+
* debug. The CALLER (adapter.send) actually performs the upload /
|
|
20
|
+
* render side-effects keyed on the route — this function does no I/O.
|
|
21
|
+
*
|
|
22
|
+
* Behavior contract (mirrors adapter.send line-for-line, locked by
|
|
23
|
+
* `feishu-outbound-render-mode.test.ts`):
|
|
24
|
+
*
|
|
25
|
+
* 1. `imagePath` set → `image_upload`
|
|
26
|
+
* 2. `files[0].path` set → `file_upload`
|
|
27
|
+
* 3. `forceTextOnly` true → `text`
|
|
28
|
+
* 4. mode "plain" → `text` (always)
|
|
29
|
+
* 5. mode "card" → `card_short_md` iff `looksLikeMarkdown(text)` AND
|
|
30
|
+
* NOT `shouldRenderAsImage(text)`; else `text`
|
|
31
|
+
* 6. mode "auto" → `image_render` iff `shouldRenderAsImage(text)`;
|
|
32
|
+
* else `card_short_md` iff `looksLikeMarkdown(text)`; else `text`
|
|
33
|
+
* 7. unknown mode (defensive default-default) → `text`
|
|
34
|
+
*
|
|
35
|
+
* The production switch in adapter.send may add the actual upload /
|
|
36
|
+
* render / lark API calls keyed on the route, but the route itself
|
|
37
|
+
* is decided HERE. To change the decision, change this file.
|
|
38
|
+
*/
|
|
39
|
+
export type OutboundRoute = "image_upload" | "file_upload" | "text" | "card_short_md" | "image_render";
|
|
40
|
+
export interface RouteInput {
|
|
41
|
+
text?: string;
|
|
42
|
+
imagePath?: string;
|
|
43
|
+
files?: {
|
|
44
|
+
name: string;
|
|
45
|
+
path?: string;
|
|
46
|
+
}[];
|
|
47
|
+
forceTextOnly?: boolean;
|
|
48
|
+
/** `outboundRender` from the channel config. Defaults to "plain" when
|
|
49
|
+
* absent so a caller that doesn't read the config still gets the
|
|
50
|
+
* "Vincent default" behavior. */
|
|
51
|
+
mode?: "plain" | "card" | "auto";
|
|
52
|
+
}
|
|
53
|
+
export interface RouteDecision {
|
|
54
|
+
route: OutboundRoute;
|
|
55
|
+
reason: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Decide the outbound route for a `NormalizedIMMessage` (or a subset
|
|
59
|
+
* of its fields). Pure — no I/O, no SDK calls; safe to call from tests
|
|
60
|
+
* directly.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveOutboundRoute(input: RouteInput): RouteDecision;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface WorkerProcessBoundary {
|
|
2
|
+
stderr: {
|
|
3
|
+
write(message: string): unknown;
|
|
4
|
+
};
|
|
5
|
+
exit(code: number): unknown;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A terminal WS error means this worker no longer owns a live ingress path.
|
|
9
|
+
* Exit non-zero so the parent/supervisor cannot continue advertising it as
|
|
10
|
+
* healthy. Kept separate from worker.ts so the real exit contract is testable.
|
|
11
|
+
*/
|
|
12
|
+
export declare function exitFeishuWorker(error: Error, boundary?: WorkerProcessBoundary): void;
|
|
13
|
+
export {};
|