@sleep2agi/agent-network 2.3.0-preview.7 → 2.3.0-preview.71
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/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 +10 -0
- package/dist/src/claude-vendor-env.d.ts +28 -0
- package/dist/src/cli-args.d.ts +8 -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 +58 -0
- package/dist/src/daemon-capability-display.js +9 -0
- package/dist/src/dashboard-managed-process.d.ts +35 -0
- package/dist/src/environ-alias.d.ts +14 -0
- package/dist/src/grok-attach-client.d.ts +129 -0
- package/dist/src/grok-copresence-disclosure.d.ts +11 -0
- package/dist/src/grok-copresence-orchestration.d.ts +70 -0
- package/dist/src/grok-copresence-profile.d.ts +65 -0
- package/dist/src/im/correlation-store.d.ts +34 -0
- package/dist/src/im/feishu/adapter.d.ts +118 -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/markdown-image-renderer.d.ts +61 -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 +403 -28
- package/dist/src/im/types.d.ts +38 -1
- package/dist/src/locale-diagnostic.d.ts +12 -0
- package/dist/src/mock-llm.d.ts +12 -0
- package/dist/src/node-activity-log.d.ts +4 -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/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-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/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/stale-socket.d.ts +118 -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 +41 -0
- package/package.json +14 -6
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** tmux session names. The attachable TUI owns the bare alias so that
|
|
2
|
+
* `tmux attach -t '=<alias>'` lands a human on the TUI, matching the codex
|
|
3
|
+
* lane exactly — an operator should not have to remember which runtime a node
|
|
4
|
+
* uses in order to know where to attach. */
|
|
5
|
+
export declare function grokCopresenceSessions(displayName: string): {
|
|
6
|
+
node: string;
|
|
7
|
+
tui: string;
|
|
8
|
+
};
|
|
9
|
+
export type GrokAttachSocketState = "missing" | "not_a_socket" | "ready";
|
|
10
|
+
/** Readiness is "the attach socket exists AND is a socket", never "the child
|
|
11
|
+
* process started". A grok leader that dies during startup leaves either no
|
|
12
|
+
* socket at all or a stale regular file from an aborted run; both used to read
|
|
13
|
+
* as ready because nothing looked. */
|
|
14
|
+
export declare function grokAttachSocketState(entry: {
|
|
15
|
+
isSocket(): boolean;
|
|
16
|
+
} | null | undefined): GrokAttachSocketState;
|
|
17
|
+
/** Set on the node child so that `anet node start` inside the tmux session does
|
|
18
|
+
* not re-enter this orchestration and fork bomb. The codex lane guards on the
|
|
19
|
+
* same variable (cli.ts: `process.env.ANET_COPRESENCE_BRIDGE !== "1"`); using a
|
|
20
|
+
* second name would leave two guards that must be kept in sync by hand. */
|
|
21
|
+
export declare const GROK_COPRESENCE_CHILD_ENV = "ANET_COPRESENCE_BRIDGE";
|
|
22
|
+
export interface GrokCopresenceDiagnosis {
|
|
23
|
+
readonly ok: boolean;
|
|
24
|
+
readonly lines: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
/** One block naming every reason this node cannot run the shared TUI, not one
|
|
27
|
+
* exit per reason. A node that is both the wrong runtime AND missing its
|
|
28
|
+
* attach socket should learn both in one run. */
|
|
29
|
+
/** Platforms whose PTY / IPC / isolation primitives the grok co-presence lane
|
|
30
|
+
* has actually been validated on. agent-node refuses anywhere else, and it does
|
|
31
|
+
* so only after the node has been created and started once:
|
|
32
|
+
*
|
|
33
|
+
* [agent-node] grok-build-cli co-presence 无法在 darwin 上运行,缺少:
|
|
34
|
+
* 平台 darwin 尚未验证过共存所需的 PTY / IPC / 隔离原语
|
|
35
|
+
*
|
|
36
|
+
* Learned the expensive way on a Mac mini: install anet, install agent-node,
|
|
37
|
+
* create the node, start it — and only then find out the platform is refused.
|
|
38
|
+
* A one-command launcher that lets someone get that far has not saved them
|
|
39
|
+
* anything, so say it in the same breath as every other gap. */
|
|
40
|
+
export declare const GROK_COPRESENCE_PLATFORMS: readonly NodeJS.Platform[];
|
|
41
|
+
export declare function diagnoseGrokCopresence(input: {
|
|
42
|
+
runtime: string;
|
|
43
|
+
displayName: string;
|
|
44
|
+
grokCopresence?: boolean;
|
|
45
|
+
grokAttachSocket?: string;
|
|
46
|
+
platform?: NodeJS.Platform;
|
|
47
|
+
}): GrokCopresenceDiagnosis;
|
|
48
|
+
/** True when a plain `anet node start <name>` (no flag) should bring the shared
|
|
49
|
+
* TUI up by itself.
|
|
50
|
+
*
|
|
51
|
+
* 🔴 Deliberately NOT keyed on `grokCopresence`. That field is set to true at
|
|
52
|
+
* CREATE time for every grok-build-cli node, so reading it here would silently
|
|
53
|
+
* change what `anet node start` does for every grok node that already exists —
|
|
54
|
+
* they would stop running in the foreground and fork into tmux, unasked. Caught
|
|
55
|
+
* exactly that way: a start without the flag entered the orchestration.
|
|
56
|
+
*
|
|
57
|
+
* The codex lane looks the same but is not: `codexCopresence` is written only
|
|
58
|
+
* when the operator passes the flag. So grok needs its own record of the
|
|
59
|
+
* operator's choice, and `grokCopresenceAuto` is it. */
|
|
60
|
+
export declare function grokCopresenceRequested(flagPassed: boolean, profile: {
|
|
61
|
+
runtime?: unknown;
|
|
62
|
+
grokCopresence?: unknown;
|
|
63
|
+
grokCopresenceAuto?: unknown;
|
|
64
|
+
}): boolean;
|
|
65
|
+
/** Record the operator's opt-in so the NEXT start needs no flag — and only then.
|
|
66
|
+
* Never infer it from `grokCopresence`, which create sets by default. */
|
|
67
|
+
export declare function shouldPersistGrokCopresence(flagPassed: boolean, profile: {
|
|
68
|
+
runtime?: unknown;
|
|
69
|
+
grokCopresenceAuto?: unknown;
|
|
70
|
+
}): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export declare const GROK_UNIX_SOCKET_PATH_MAX_BYTES = 100;
|
|
2
|
+
export declare const GROK_COPRESENCE_CAPABILITY_MARKER = "ANET_CAPABILITY_GROK_COPRESENCE_V2";
|
|
3
|
+
export declare const GROK_PREVIEW_RESOLVER_INHERITED_ENV_KEYS: readonly ["PATH", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "LC_CTYPE", "TZ"];
|
|
4
|
+
export declare const GROK_AGENT_NODE_INHERITED_ENV_KEYS: readonly ["PATH", "HOME", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "LC_CTYPE", "TZ", "SHELL", "USER", "LOGNAME", "TERM", "COLORTERM", "NO_COLOR"];
|
|
5
|
+
export declare const GROK_AGENT_NODE_OPTIONAL_ENV_KEYS: readonly ["GROK_BINARY", "GROK_HOME", "FLOCK_BINARY", "SETPRIV_BINARY", "UNSHARE_BINARY", "GROK_CLI_TIMEOUT_MS", "GROK_HANDSHAKE_TIMEOUT_MS", "LOG_LEVEL", "ANET_GOAL_TICK_MS", "COMMHUB_MAX_GOALS_PER_NODE"];
|
|
6
|
+
/** Exact environment for the long-lived agent-node parent of the Grok TUI. */
|
|
7
|
+
export declare function buildGrokAgentNodeEnv(parentEnv: NodeJS.ProcessEnv): Record<string, string>;
|
|
8
|
+
export declare function grokPreviewResolverConfigPaths(home: string): {
|
|
9
|
+
directory: string;
|
|
10
|
+
userConfig: string;
|
|
11
|
+
globalConfig: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* npm rejects loading the same file as both user and global config. Prepare
|
|
15
|
+
* two distinct, empty, owner-only files without following a final symlink so
|
|
16
|
+
* the resolver cannot inherit a user's ordinary npmrc credentials.
|
|
17
|
+
*/
|
|
18
|
+
export declare function prepareGrokPreviewResolverConfigs(home: string): void;
|
|
19
|
+
/** Exact environment for the short-lived npm resolver and capability probe. */
|
|
20
|
+
export declare function buildGrokPreviewResolverEnv(parentEnv: NodeJS.ProcessEnv, home: string): Record<string, string>;
|
|
21
|
+
/** Old headless-only agent-node builds already advertised grok-build-cli. */
|
|
22
|
+
export declare function agentNodeHelpSupportsGrokCopresence(help: string): boolean;
|
|
23
|
+
export interface GrokCopresenceProfileFields {
|
|
24
|
+
grokCopresence: boolean;
|
|
25
|
+
grokLeaderSocket?: string;
|
|
26
|
+
grokAttachSocket?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface GrokSocketPathOptions {
|
|
29
|
+
cwd?: string;
|
|
30
|
+
home?: string;
|
|
31
|
+
xdgRuntimeDir?: string;
|
|
32
|
+
uid?: number;
|
|
33
|
+
platform?: NodeJS.Platform;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Allocate deterministic Unix socket paths without creating anything.
|
|
37
|
+
*
|
|
38
|
+
* Grok's workspace sandbox does not admit an otherwise owner-controlled
|
|
39
|
+
* XDG_RUNTIME_DIR such as /run/user/<uid>. Keep the primary sockets under the
|
|
40
|
+
* node's owner-bound state home, which the runtime already admits, and use a
|
|
41
|
+
* short private tmp path only when the Unix socket length limit requires it.
|
|
42
|
+
* The runtime owns directory creation and permissions; `anet node create`
|
|
43
|
+
* only persists the identity of the two sockets.
|
|
44
|
+
*/
|
|
45
|
+
export declare function grokCopresenceSocketPaths(nodeId: string, options?: GrokSocketPathOptions): {
|
|
46
|
+
leaderSocket: string;
|
|
47
|
+
attachSocket: string;
|
|
48
|
+
};
|
|
49
|
+
export type GrokAttachTarget = {
|
|
50
|
+
ok: true;
|
|
51
|
+
socketPath: string;
|
|
52
|
+
} | {
|
|
53
|
+
ok: false;
|
|
54
|
+
reason: "not_grok_build_cli" | "headless" | "missing_attach_socket";
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* `anet grok attach` eligibility. The CLI is the only human join path;
|
|
58
|
+
* this is the shipped decision so tests can drive it without a TTY.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveGrokAttachTarget(input: {
|
|
61
|
+
runtime: string;
|
|
62
|
+
grokCopresence?: unknown;
|
|
63
|
+
grokAttachSocket?: unknown;
|
|
64
|
+
}): GrokAttachTarget;
|
|
65
|
+
export declare function grokBuildCliCreationFields(runtime: string, nodeId: string, headless?: boolean, options?: GrokSocketPathOptions): GrokCopresenceProfileFields | Record<string, never>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { IMCorrelationStore, IMTaskCorrelation } from "./types";
|
|
2
|
+
export interface JsonIMCorrelationStoreOptions {
|
|
3
|
+
now?: () => number;
|
|
4
|
+
/** How long idempotency keys remain valid. Defaults to 24h. */
|
|
5
|
+
seenTtlMs?: number;
|
|
6
|
+
/** How long terminal correlations remain queryable. Defaults to 24h. */
|
|
7
|
+
terminalTtlMs?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Small durable store for RFC-020 §2.9④ / §4.4 correlation state.
|
|
11
|
+
*
|
|
12
|
+
* The first gateway PR needs this as a standalone data structure before the
|
|
13
|
+
* bridge is rewired: idempotency keys survive process restart, and task replies
|
|
14
|
+
* can be routed back to their originating IM conversation.
|
|
15
|
+
*/
|
|
16
|
+
export declare class JsonIMCorrelationStore implements IMCorrelationStore {
|
|
17
|
+
private readonly path;
|
|
18
|
+
private readonly now;
|
|
19
|
+
private readonly seenTtlMs;
|
|
20
|
+
private readonly terminalTtlMs;
|
|
21
|
+
constructor(path: string, options?: JsonIMCorrelationStoreOptions);
|
|
22
|
+
hasSeen(idempotencyKey: string): Promise<string | null>;
|
|
23
|
+
recordSeen(idempotencyKey: string, taskId: string): Promise<void>;
|
|
24
|
+
getCorrelation(taskId: string): Promise<IMTaskCorrelation | null>;
|
|
25
|
+
putCorrelation(taskId: string, correlation: IMTaskCorrelation): Promise<void>;
|
|
26
|
+
updateStatus(taskId: string, status: IMTaskCorrelation["status"]): Promise<void>;
|
|
27
|
+
gc(now: number): Promise<{
|
|
28
|
+
removed: number;
|
|
29
|
+
}>;
|
|
30
|
+
private isExpired;
|
|
31
|
+
private load;
|
|
32
|
+
private save;
|
|
33
|
+
}
|
|
34
|
+
export declare function createJsonIMCorrelationStore(path: string, options?: JsonIMCorrelationStoreOptions): IMCorrelationStore;
|
|
@@ -1,12 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-020 §3.1 — Feishu (Lark) adapter for the IM compatibility layer.
|
|
3
|
+
*
|
|
4
|
+
* Uses `@larksuiteoapi/node-sdk` in WebSocket long-connection mode (WSClient).
|
|
5
|
+
* No public IP / no domain verification / no webhook signature decryption —
|
|
6
|
+
* the three biggest 飞书 接入 risks all live in the HTTP event-callback path,
|
|
7
|
+
* not in WSClient mode.
|
|
8
|
+
*
|
|
9
|
+
* Milestones:
|
|
10
|
+
* M1: contract scaffold.
|
|
11
|
+
* M2 (this file): WSClient init + EventDispatcher for `im.message.receive_v1`
|
|
12
|
+
* + event normalization + access whitelist gate + audit log.
|
|
13
|
+
* M3: outbound `im.message.create` (text), edit support (≤20/msg).
|
|
14
|
+
* M5: image upload / download (`im.image.create` / `im.messageResource.get`)
|
|
15
|
+
* + group @bot detection refined to match the bot's own open_id.
|
|
16
|
+
*/
|
|
17
|
+
import * as lark from "@larksuiteoapi/node-sdk";
|
|
1
18
|
import type { IMAdapter, IMAdapterHealth, IMChannelConfig, IMConversationRef, IMIngressMode, NormalizedIMEvent, NormalizedIMMessage } from "../types.js";
|
|
2
19
|
type OnEventHandler = (event: NormalizedIMEvent) => Promise<void>;
|
|
20
|
+
export type FeishuWsClientLike = Pick<lark.WSClient, "start" | "close">;
|
|
21
|
+
export type FeishuWsClientFactory = (params: ConstructorParameters<typeof lark.WSClient>[0]) => FeishuWsClientLike;
|
|
22
|
+
type FeishuInboundHandler = (rawEvent: unknown) => Promise<unknown>;
|
|
23
|
+
export interface FeishuEventDispatcherLike {
|
|
24
|
+
register(handlers: Record<string, FeishuInboundHandler>): unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface FeishuAdapterOptions {
|
|
27
|
+
/** @internal Avoids real bot-info HTTP calls in lifecycle tests. */
|
|
28
|
+
createClient?: (params: ConstructorParameters<typeof lark.Client>[0]) => lark.Client;
|
|
29
|
+
/** @internal Test seam; production uses the pinned Lark SDK WSClient. */
|
|
30
|
+
createWsClient?: FeishuWsClientFactory;
|
|
31
|
+
/** @internal Test seam; production uses the pinned Lark SDK dispatcher. */
|
|
32
|
+
createEventDispatcher?: () => FeishuEventDispatcherLike;
|
|
33
|
+
/** Independent outer bound in case the SDK promise/callback path stalls. */
|
|
34
|
+
wsReadyTimeoutMs?: number;
|
|
35
|
+
/** Called once when an already-ready socket exhausts reconnect attempts. */
|
|
36
|
+
onTerminalError?: (error: Error) => void;
|
|
37
|
+
}
|
|
3
38
|
export declare class FeishuAdapter implements IMAdapter {
|
|
4
39
|
readonly platform = "feishu";
|
|
5
40
|
readonly ingressMode: IMIngressMode;
|
|
6
41
|
private feishuConfig;
|
|
7
|
-
private
|
|
42
|
+
private connectionName_;
|
|
8
43
|
private client;
|
|
9
44
|
private wsClient;
|
|
45
|
+
private lifecycleGeneration;
|
|
46
|
+
private readonly options;
|
|
10
47
|
/**
|
|
11
48
|
* The bot's own open_id, resolved at init() via /open-apis/bot/v3/info.
|
|
12
49
|
* Used to detect real @bot mentions (vs any mention) in group messages.
|
|
@@ -17,6 +54,7 @@ export declare class FeishuAdapter implements IMAdapter {
|
|
|
17
54
|
/** Where to persist downloaded inbound media (M5c). */
|
|
18
55
|
private mediaDir;
|
|
19
56
|
private health_;
|
|
57
|
+
constructor(options?: FeishuAdapterOptions);
|
|
20
58
|
/**
|
|
21
59
|
* Snapshot of the current `access.allowFrom` list (from access.json).
|
|
22
60
|
* Used by the bridge's rate-limiter to exempt operator-vouched explicit
|
|
@@ -28,6 +66,12 @@ export declare class FeishuAdapter implements IMAdapter {
|
|
|
28
66
|
* the public-channel shape and still needs flood protection.
|
|
29
67
|
*/
|
|
30
68
|
getAllowFrom(): readonly string[];
|
|
69
|
+
/**
|
|
70
|
+
* Read-only accessor used by bridge to resolve per-connection paths
|
|
71
|
+
* (RFC-020 §15 outbound-marker validation needs the connection name
|
|
72
|
+
* to build the allowed per-conversation directory prefix).
|
|
73
|
+
*/
|
|
74
|
+
get connectionName(): string;
|
|
31
75
|
init(config: IMChannelConfig): Promise<void>;
|
|
32
76
|
start(onEvent: OnEventHandler): Promise<void>;
|
|
33
77
|
stop(): Promise<void>;
|
|
@@ -57,4 +101,77 @@ export declare class FeishuAdapter implements IMAdapter {
|
|
|
57
101
|
* `|` and `**`).
|
|
58
102
|
*/
|
|
59
103
|
export declare function looksLikeMarkdown(text: string): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Parse Feishu `message_type: "post"` content into plain text. Post
|
|
106
|
+
* content is a nested structure:
|
|
107
|
+
*
|
|
108
|
+
* { title?: string,
|
|
109
|
+
* content: Array<Array<{ tag: "text"|"img"|"a"|"at"|"emotion", ... }>>
|
|
110
|
+
* }
|
|
111
|
+
*
|
|
112
|
+
* Each top-level array entry is a paragraph; each paragraph is an array
|
|
113
|
+
* of typed segments. We flatten by:
|
|
114
|
+
* - prepending title (if present) as `<title>\n\n`
|
|
115
|
+
* - joining paragraphs with `\n\n`
|
|
116
|
+
* - joining segments within a paragraph in order
|
|
117
|
+
* - tag=text → emit segment.text as-is
|
|
118
|
+
* - tag=a → emit `[label](href)` (markdown link)
|
|
119
|
+
* - tag=at → emit `@user_name` (fallback to `@<user_id>` if no name)
|
|
120
|
+
* - tag=img → emit `[图片]` placeholder (actual download via maybeAttachImages)
|
|
121
|
+
* - tag=emotion → emit `[emoji]`
|
|
122
|
+
* - unknown tag → skip
|
|
123
|
+
*
|
|
124
|
+
* @internal exported for unit tests.
|
|
125
|
+
*/
|
|
126
|
+
export declare function parsePostContent(rawJson: string): string;
|
|
127
|
+
/**
|
|
128
|
+
* Walk a Feishu `post` content JSON and collect all `image_key` values
|
|
129
|
+
* from `tag: "img"` segments. Used by `maybeAttachImages` to schedule
|
|
130
|
+
* downloads for every image in a 图文混排 message.
|
|
131
|
+
*
|
|
132
|
+
* @internal exported for unit tests.
|
|
133
|
+
*/
|
|
134
|
+
export declare function extractPostImageKeys(rawJson: string): string[];
|
|
135
|
+
/**
|
|
136
|
+
* Sanitize a Feishu-supplied `file_name` so it's safe to append to a
|
|
137
|
+
* filesystem path. Strips `/`, `\`, `..`, control characters, and NUL
|
|
138
|
+
* bytes. Empty / all-stripped input falls back to a placeholder that
|
|
139
|
+
* uses the message id, so a hostile client can never write outside the
|
|
140
|
+
* conversation's `<mediaDir>/<convKey>/` directory.
|
|
141
|
+
*
|
|
142
|
+
* NOT a full display-safety pass — the LLM still sees the sanitized
|
|
143
|
+
* bytes and shouldn't render them as HTML/etc. That's a Layer above.
|
|
144
|
+
*/
|
|
145
|
+
export declare function sanitizeFileName(raw: string, fallback: string): string;
|
|
146
|
+
/**
|
|
147
|
+
* Feishu text-message practical chunk threshold (RFC-020 §16).
|
|
148
|
+
*
|
|
149
|
+
* The official `im.message.create`/`reply` content limit for
|
|
150
|
+
* `msg_type:text` is ~30 KB JSON-encoded (`{"text":"..."}`), comfortably
|
|
151
|
+
* under what any reasonable bot reply produces. We chunk below that
|
|
152
|
+
* limit at 4000 CHARACTERS — gives a roomy safety margin for multi-byte
|
|
153
|
+
* UTF-8 and lets us split at paragraph boundaries cleanly. Chosen
|
|
154
|
+
* conservatively after Vincent 2026-06-30 ask "issue 发文字" (i.e.
|
|
155
|
+
* never silently fall back to PNG for "long" plain-text replies — they
|
|
156
|
+
* just chunk into multiple messages).
|
|
157
|
+
*
|
|
158
|
+
* Single-message ceiling, NOT a per-second rate limit (that's separate;
|
|
159
|
+
* RFC-020 §4.4).
|
|
160
|
+
*/
|
|
161
|
+
export declare const FEISHU_TEXT_SINGLE_LIMIT = 4000;
|
|
162
|
+
/**
|
|
163
|
+
* Split a long text into chunks ≤ `maxChars`. Tries paragraph boundaries
|
|
164
|
+
* (`\n\n`), then line boundaries (`\n`), then word boundaries (space),
|
|
165
|
+
* then hard byte split. Output preserves the original text content
|
|
166
|
+
* (sum of chunks == original, modulo the boundary character that gets
|
|
167
|
+
* consumed by the split).
|
|
168
|
+
*
|
|
169
|
+
* If the input is already short enough, returns a single-element array.
|
|
170
|
+
*/
|
|
171
|
+
export declare function splitTextForFeishu(text: string, maxChars: number): string[];
|
|
172
|
+
/**
|
|
173
|
+
* Lark errors may echo request/config values. Keep worker logs actionable while
|
|
174
|
+
* ensuring credentials and multiline payloads never cross the process boundary.
|
|
175
|
+
*/
|
|
176
|
+
export declare function sanitizeFeishuWsError(rawError: unknown, appId: string, appSecret: string): Error;
|
|
60
177
|
export {};
|
|
@@ -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,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-020 §14 — markdown → image rendering for Feishu replies.
|
|
3
|
+
*
|
|
4
|
+
* Vincent 2026-06-29 path: Feishu's `markdown` card element doesn't
|
|
5
|
+
* render ATX headings or GFM tables (preview.7 caught only bold / list
|
|
6
|
+
* / link). Rather than partially support a moving subset of card
|
|
7
|
+
* elements, we render structured markdown to a PNG via headless
|
|
8
|
+
* chromium and send it through Feishu's image API (`im:resource:upload`
|
|
9
|
+
* scope). Pixel-perfect fidelity, zero schema fragility, text-not-
|
|
10
|
+
* copyable accepted as the tradeoff for "actually renders".
|
|
11
|
+
*
|
|
12
|
+
* Hybrid route (decided in adapter.send):
|
|
13
|
+
* - plain text (no markdown markers) → msg_type:"text"
|
|
14
|
+
* - markdown WITHOUT heading / table / long → msg_type:"interactive" (schema 1.0 card with `markdown` element — keeps copy/paste for short bold/list/link replies; preview.7 path)
|
|
15
|
+
* - markdown WITH heading / table / long → THIS PATH (msg_type:"image" with rendered PNG)
|
|
16
|
+
*
|
|
17
|
+
* Renderer choice: `puppeteer-core` (no bundled chromium) + system
|
|
18
|
+
* chromium (apt install in Docker). Rationale:
|
|
19
|
+
* - Pure-JS canvas-layout libraries (node-canvas, @napi-rs/canvas)
|
|
20
|
+
* cost 4-6h of manual paragraph wrapping + table cell measurement
|
|
21
|
+
* + Chinese width metrics; chromium does this natively.
|
|
22
|
+
* - puppeteer-core (5MB) + system chromium (~100MB) is smaller than
|
|
23
|
+
* full puppeteer (170MB) which bundles its own chromium.
|
|
24
|
+
* - Headless screenshot ~500ms after warmup. Bot's heavy turn is 20-
|
|
25
|
+
* 70s; rendering cost is in the noise.
|
|
26
|
+
*
|
|
27
|
+
* Chromium reuse: we keep a single browser instance hot across
|
|
28
|
+
* renderings to avoid the ~2-3s cold-start per call. Auto-close on
|
|
29
|
+
* idle is a follow-up — agent-node worker lifetime is bounded.
|
|
30
|
+
*/
|
|
31
|
+
import { Buffer } from "node:buffer";
|
|
32
|
+
/**
|
|
33
|
+
* Close the shared browser. Called from adapter.stop() during graceful
|
|
34
|
+
* worker shutdown. Subsequent render calls re-launch.
|
|
35
|
+
*/
|
|
36
|
+
export declare function closeBrowser(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Render markdown text to a PNG buffer.
|
|
39
|
+
*
|
|
40
|
+
* Width is fixed at 800px (IM-friendly); height auto-fits content via
|
|
41
|
+
* `fullPage:true`. Returns the raw PNG bytes — caller hands them to
|
|
42
|
+
* `lark.im.image.create({image: Readable.from(buffer)})`.
|
|
43
|
+
*
|
|
44
|
+
* Throws on chromium launch failure or page navigation failure.
|
|
45
|
+
* Caller is responsible for fallback (e.g., send the raw text as a
|
|
46
|
+
* code block in the schema 1.0 card path).
|
|
47
|
+
*/
|
|
48
|
+
export declare function renderMarkdownToPng(text: string): Promise<Buffer>;
|
|
49
|
+
/**
|
|
50
|
+
* Decide whether `renderMarkdownToPng` should be invoked for this reply
|
|
51
|
+
* text, vs. falling back to msg_type:"text" / msg_type:"interactive"
|
|
52
|
+
* (schema 1.0 card). Locked by 通信龙 3f70044c — trigger image when:
|
|
53
|
+
* - text contains a markdown table (Feishu card can't render)
|
|
54
|
+
* - text contains an ATX heading (Feishu card can't render)
|
|
55
|
+
* - text length > 2000 chars (image is better than scrollable text wall)
|
|
56
|
+
*
|
|
57
|
+
* Returns false for short prose, single-line plain text, and short
|
|
58
|
+
* markdown lists / bold / link / inline-code which DO render fine in
|
|
59
|
+
* the schema 1.0 `markdown` card element.
|
|
60
|
+
*/
|
|
61
|
+
export declare function shouldRenderAsImage(text: string): boolean;
|