localsnake-sdk-web 2026.8.7-v1

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 ADDED
@@ -0,0 +1,262 @@
1
+ # LocalSnake SDK Web
2
+
3
+ `localsnake-sdk-web` is the JavaScript SDK for LocalSnake main-interface apps. It exposes two top-level objects:
4
+
5
+ - `LocalSnake`: app lifecycle, Agent chat, realtime events, workspace files, and low-level host requests.
6
+ - `LocalHub`: LocalHub agent, LLM, proxy, and WebSocket proxy APIs.
7
+
8
+ Docs: English | [简体中文](README.zh-CN.md) | [Architecture](Architecture.md)
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install localsnake-sdk-web
14
+ ```
15
+
16
+ Browser bundle:
17
+
18
+ ```html
19
+ <script src="https://cdn.example.com/localsnake-sdk-web.min.js"></script>
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```ts
25
+ import { LocalSnake } from 'localsnake-sdk-web';
26
+
27
+ const context = await LocalSnake.init();
28
+ console.log(context.agentId, context.sessionKey);
29
+
30
+ LocalSnake.onEvent((event) => {
31
+ if (event.type === 'runtime_status') {
32
+ console.log(event.payload.status, event.payload.activityType);
33
+ }
34
+ });
35
+
36
+ await LocalSnake.sendMessage('帮我查一下今天的安排');
37
+ ```
38
+
39
+ ## Browser Usage
40
+
41
+ ```html
42
+ <script src="https://cdn.example.com/localsnake-sdk-web.min.js"></script>
43
+ <script>
44
+ const { LocalSnake, LocalHub } = window;
45
+
46
+ LocalSnake.onEvent((event) => {
47
+ if (event.type === 'runtime_status') {
48
+ console.log(event.payload.status, event.payload.activityType);
49
+ }
50
+ });
51
+
52
+ LocalSnake.init((context) => {
53
+ console.log('LocalSnake app context', context);
54
+ });
55
+
56
+ async function ask(text) {
57
+ await LocalSnake.sendMessage(text);
58
+ }
59
+
60
+ async function listModels() {
61
+ return LocalHub.llm.request('/v1/models');
62
+ }
63
+ </script>
64
+ ```
65
+
66
+ ## Module Usage
67
+
68
+ ```ts
69
+ import { LocalSnake, LocalHub } from 'localsnake-sdk-web';
70
+
71
+ LocalSnake.onEvent((event) => {
72
+ console.log(event.type, event.payload);
73
+ });
74
+
75
+ await LocalSnake.init();
76
+ await LocalSnake.sendMessage('帮我查一下今天的安排');
77
+
78
+ const metadata = await LocalSnake.workspace.readJson('works/montage/knowledge/metadata.json');
79
+ const models = await LocalHub.llm.request('/v1/models');
80
+ ```
81
+
82
+ Default import returns `LocalSnake`:
83
+
84
+ ```ts
85
+ import LocalSnake from 'localsnake-sdk-web';
86
+ ```
87
+
88
+ ## Lifecycle
89
+
90
+ | API | Trigger | Callback argument | Typical use |
91
+ | --- | --- | --- | --- |
92
+ | `LocalSnake.init(handler?)` | After page load and the host `app.init` handshake. If the WebView is visible, `onShow()` is replayed after init. | `LocalSnakeAppContext` | Read agent/session/workspace state and initialize the app. |
93
+ | `LocalSnake.onShow(handler)` | WebView first shown or restored from hidden state. | none | Resume animation, rendering, polling, or visible-state sync. |
94
+ | `LocalSnake.onHide(handler)` | WebView hidden. | none | Pause animation, rendering, or expensive recalculation. |
95
+ | `LocalSnake.onDestroy(handler)` | WebView is unloading, detached, or switching app / agent / page. | none | Cleanup timers, WebGL, audio, and subscriptions. |
96
+
97
+ Lifecycle callbacks are not delivered through public `LocalSnake.onEvent()`.
98
+
99
+ ## Common APIs
100
+
101
+ | API | Return value | Notes |
102
+ | --- | --- | --- |
103
+ | `LocalSnake.onEvent(handler)` | `() => void` | Subscribes to realtime non-lifecycle host events. The SDK may replay the latest `runtime_status` so apps do not miss the initial state. |
104
+ | `LocalSnake.sendMessage(text, options?)` | `Promise<unknown>` | Sends a user message to the current open session. Pass `options.sessionKey` to target a specific session. |
105
+ | `LocalSnake.getHistory()` | currently unsupported | Reserved for a future explicit history API. |
106
+ | `LocalSnake.workspace.readText(path)` | `Promise<string \| null>` | Reads a UTF-8 text file under the current agent workspace. Missing files return `null`. |
107
+ | `LocalSnake.workspace.readJson<T>(path)` | `Promise<T \| null>` | Reads and parses a JSON file under the current agent workspace. Missing files return `null`; invalid JSON rejects. |
108
+ | `LocalSnake.workspace.writeText(path, content)` | `Promise<{ path: string, success: boolean, error?: string }>` | Writes a UTF-8 text file under the current agent workspace. Parent directories are created by the host. |
109
+ | `LocalSnake.workspace.writeJson(path, value)` | `Promise<{ path: string, success: boolean, error?: string }>` | Serializes JSON with two-space indentation and writes it under the current agent workspace. |
110
+ | `LocalSnake.request(method, payload?)` | `Promise<T>` | Low-level request entry. Prefer wrapped APIs for normal use. |
111
+ | `LocalHub.request(pathOrRequest, init?)` | `Promise<LocalSnakeLocalHubResponse>` | Low-level LocalHub proxy entry. Prefer the typed `LocalHub.agent`, `LocalHub.llm`, and `LocalHub.proxy` helpers. |
112
+
113
+ ## Send Message
114
+
115
+ ```ts
116
+ await LocalSnake.sendMessage('Summarize this workspace');
117
+
118
+ await LocalSnake.sendMessage('Send this to a child session', {
119
+ sessionKey: 'session-key',
120
+ });
121
+ ```
122
+
123
+ If `sessionKey` is omitted, LocalSnake sends the message to the currently active session.
124
+
125
+ ## Workspace Files
126
+
127
+ `LocalSnake.workspace` is scoped to the current agent workspace reported by `init()`. Paths must be relative workspace paths such as `works/montage/knowledge/metadata.json`; absolute paths, empty paths, null bytes, and `..` escapes are rejected by the host.
128
+
129
+ ```ts
130
+ const index = await LocalSnake.workspace.readJson('works/montage/knowledge/metadata.json');
131
+ await LocalSnake.workspace.writeJson('works/montage/knowledge/metadata.json', {
132
+ schema_version: 1,
133
+ kind: 'dreamer.knowledge.index',
134
+ updated_at: new Date().toISOString(),
135
+ projects: index?.projects ?? [],
136
+ });
137
+ ```
138
+
139
+ ## LocalHub
140
+
141
+ `LocalHub` is split into the same three public LocalHub route domains:
142
+
143
+ | API | Route prefix | Use |
144
+ | --- | --- | --- |
145
+ | `LocalHub.agent.request(pathOrRequest, init?)` | `/agent/*` | LocalHub management, search bridge, and evolution APIs. |
146
+ | `LocalHub.llm.request(pathOrRequest, init?)` | `/llm/*` | LLM proxy calls, including OpenAI-compatible and Anthropic-compatible routes. |
147
+ | `LocalHub.proxy.request(pathOrRequest, init?)` | `/proxy/*` | HTTP request-proxy calls configured in LocalHub. |
148
+ | `LocalHub.proxy.route(routes)` | `/agent/proxies` | Overwrite request-proxy route configuration for the system LocalHub key. Pass `[]` to clear. |
149
+ | `LocalHub.proxy.websocket(pathOrRequest, options?)` | `/proxy/*` | WebSocket request-proxy calls configured in LocalHub. |
150
+
151
+ LocalSnake supplies the LocalHub base URL and injects the configured `localHubApiKey` as `Authorization: Bearer <key>` in the background. Apps should not read, store, or pass `localHubApiKey`.
152
+
153
+ ```ts
154
+ await LocalHub.agent.request('/initialized');
155
+ await LocalHub.llm.request('/v1/models');
156
+
157
+ const res = await LocalHub.proxy.request('/orders/42', {
158
+ method: 'POST',
159
+ body: { status: 'paid' },
160
+ });
161
+ ```
162
+
163
+ Request-proxy routes can be replaced for the system LocalHub key with a semantic helper. The LocalHub management endpoint is `/agent/proxies`, but the SDK exposes it under `LocalHub.proxy.route()` because it configures runtime `/proxy/*` routes.
164
+
165
+ ```ts
166
+ await LocalHub.proxy.route([
167
+ {
168
+ name: 'orders',
169
+ protocol: 'http',
170
+ path: '/proxy/orders/*',
171
+ targetUrl: 'https://api.example.com/orders',
172
+ headers: { Authorization: 'Bearer upstream-token' },
173
+ },
174
+ ]);
175
+
176
+ await LocalHub.proxy.route([]);
177
+ ```
178
+
179
+ WebSocket request proxies are exposed through a small browser-like object. It is backed by the host bridge so LocalSnake can attach the required Authorization header during the LocalHub handshake.
180
+
181
+ ```ts
182
+ const socket = LocalHub.proxy.websocket('/events');
183
+
184
+ socket.onopen = () => socket.send('hello');
185
+ socket.onmessage = (event) => {
186
+ console.log(event.data);
187
+ };
188
+ socket.onclose = (event) => {
189
+ console.log(event.code, event.reason);
190
+ };
191
+ ```
192
+
193
+ ## Realtime Event Contract
194
+
195
+ `LocalSnake.onEvent()` receives a single event object:
196
+
197
+ | Field | Type | Description |
198
+ | --- | --- | --- |
199
+ | `type` | `string` | Event type. |
200
+ | `timestamp` | `number` | Host-side timestamp in milliseconds. |
201
+ | `payload` | `object` | Event payload. Shape depends on `type`. |
202
+
203
+ `LocalSnake.onEvent()` events are self-contained and do not include `LocalSnakeAppContext`; read agent / session / workspace from `init()`.
204
+
205
+ | Event type | Source | Payload shape | Description |
206
+ | --- | --- | --- | --- |
207
+ | `user_message` | OpenClaw session | `{ message: LocalSnakeMessage }` | A realtime user message entered the current open session. |
208
+ | `assistant_delta` | OpenClaw stream | `{ text: string, message?: LocalSnakeMessage, runId?: string \| null }` | Realtime assistant text delta. Append it to the current message / run only. |
209
+ | `assistant_message` | OpenClaw session | `{ message: LocalSnakeMessage }` | Realtime final assistant message. |
210
+ | `tool_call` | OpenClaw tool call | `{ toolCallId?: string, toolName?: string, input?: unknown, status?: "running" \| "completed" \| "error" }` | Realtime tool call event. Do not append it into assistant text. |
211
+ | `tool_result` | OpenClaw tool result | `{ toolCallId?: string, toolName?: string, result?: unknown, text?: string, isError?: boolean }` | Realtime tool response / result. Hosts may emit more than one result event for streamed tool output. |
212
+ | `send_message` | SDK / AppManager | `{ text: string, sessionKey?: string \| null }` | The embedded app submitted a user message through `LocalSnake.sendMessage()`. |
213
+ | `runtime_status` | AppManager | `LocalSnakeRuntimeStatus` | Current-session runtime status. Prefer this event for scene / state mapping. |
214
+
215
+ ## Payload Types
216
+
217
+ ### `LocalSnakeAppContext`
218
+
219
+ | Field | Type | Description |
220
+ | --- | --- | --- |
221
+ | `app` | `"LocalSnake"` | Host app name. |
222
+ | `platform` | `"desktop" \| "mobile" \| "unknown"` | Host platform. |
223
+ | `bridge` | `"electron-webview" \| "ios-webkit" \| "android-webview" \| "react-native-webview" \| "webview2" \| "post-message" \| "unknown"` | JS bridge type. |
224
+ | `agentId` | `string \| null` | Current agent id. |
225
+ | `sessionKey` | `string \| null` | Current open session key. |
226
+ | `workspacePath` | `string \| null` | Current agent workspace path. |
227
+ | `visibilityState` | `"visible" \| "hidden"` | WebView visibility at init time. |
228
+ | `capabilities.chat` | `boolean` | Whether `sendMessage` is supported. |
229
+ | `capabilities.history` | `boolean` | Currently `false`; history is not exposed through SDK Web. |
230
+ | `capabilities.localHub` | `boolean` | Whether LocalHub proxy APIs are supported. |
231
+ | `capabilities.workspace` | `boolean` | Whether current-agent workspace file APIs are supported. |
232
+ | `capabilities.events` | `string[]` | Non-lifecycle event types the host may emit through `onEvent()`. |
233
+
234
+ ### `LocalSnakeRuntimeStatus`
235
+
236
+ | Field | Type | Description |
237
+ | --- | --- | --- |
238
+ | `status` | `"idle" \| "working" \| "subagent"` | Coarse runtime state. Ordinary main-process tool calls remain `working`; `subagent` means subagent coordination is active. |
239
+ | `activityType` | `"idle" \| "message" \| "tool" \| "subagent" \| "queued" \| "thinking"` | More specific current activity. Prefer this for UI mapping when possible. |
240
+ | `sessionKey` | `string \| null \| undefined` | Current session key. |
241
+ | `activeRunId` | `string \| null \| undefined` | Active runtime run id when known. |
242
+ | `sending` | `boolean` | Whether the current session is considered actively running. |
243
+ | `pendingFinal` | `boolean` | Whether the host is waiting for the final assistant response. |
244
+ | `queuedCount` | `number` | Count of queued steer / follow-up messages. |
245
+ | `runningTools` | `Array<{ toolCallId?: string, toolName?: string, status?: "running" \| "completed" \| "error" }>` | Running tools currently known to the host. |
246
+ | `subagentCount` | `number?` | Count of running tools identified as subagent / delegate / spawn activity. |
247
+ | `reason` | `string` | Diagnostic reason, such as `idle`, `main-session-active`, `session-run-active`, or `subagent-tools-running`. |
248
+
249
+ ### `LocalSnakeMessage`
250
+
251
+ | Field | Type | Description |
252
+ | --- | --- | --- |
253
+ | `id` | `string?` | Message id when known. |
254
+ | `role` | `"user" \| "assistant" \| "system" \| "toolResult" \| "tool"` | Normalized message role. |
255
+ | `content` | `unknown` | Message content as provided by the host / runtime. |
256
+ | `timestamp` | `number?` | Message timestamp when known. |
257
+ | `model` | `string?` | Model id when known. |
258
+ | `provider` | `string?` | Provider name when known. |
259
+ | `toolCallId` | `string?` | Tool call id when known. |
260
+ | `toolName` | `string?` | Tool name when known. |
261
+ | `isError` | `boolean?` | Whether the message is an error result. |
262
+ | `usage` | `unknown` | Token usage or other runtime metadata when known. |
@@ -0,0 +1,262 @@
1
+ # LocalSnake SDK Web
2
+
3
+ `localsnake-sdk-web` 是 LocalSnake 主界面应用的 JavaScript SDK。它向浏览器应用和前端框架应用暴露两个顶层对象:
4
+
5
+ - `LocalSnake`:应用生命周期、Agent 对话、实时事件、工作区文件和底层 Host 请求。
6
+ - `LocalHub`:LocalHub agent、LLM、proxy 和 WebSocket proxy API。
7
+
8
+ 文档:[English](README.md) | 简体中文 | [Architecture](Architecture.md)
9
+
10
+ ## 安装
11
+
12
+ ```bash
13
+ npm install localsnake-sdk-web
14
+ ```
15
+
16
+ 浏览器 bundle:
17
+
18
+ ```html
19
+ <script src="https://cdn.example.com/localsnake-sdk-web.min.js"></script>
20
+ ```
21
+
22
+ ## 快速开始
23
+
24
+ ```ts
25
+ import { LocalSnake } from 'localsnake-sdk-web';
26
+
27
+ const context = await LocalSnake.init();
28
+ console.log(context.agentId, context.sessionKey);
29
+
30
+ LocalSnake.onEvent((event) => {
31
+ if (event.type === 'runtime_status') {
32
+ console.log(event.payload.status, event.payload.activityType);
33
+ }
34
+ });
35
+
36
+ await LocalSnake.sendMessage('帮我查一下今天的安排');
37
+ ```
38
+
39
+ ## 浏览器使用
40
+
41
+ ```html
42
+ <script src="https://cdn.example.com/localsnake-sdk-web.min.js"></script>
43
+ <script>
44
+ const { LocalSnake, LocalHub } = window;
45
+
46
+ LocalSnake.onEvent((event) => {
47
+ if (event.type === 'runtime_status') {
48
+ console.log(event.payload.status, event.payload.activityType);
49
+ }
50
+ });
51
+
52
+ LocalSnake.init((context) => {
53
+ console.log('LocalSnake app context', context);
54
+ });
55
+
56
+ async function ask(text) {
57
+ await LocalSnake.sendMessage(text);
58
+ }
59
+
60
+ async function listModels() {
61
+ return LocalHub.llm.request('/v1/models');
62
+ }
63
+ </script>
64
+ ```
65
+
66
+ ## 模块化使用
67
+
68
+ ```ts
69
+ import { LocalSnake, LocalHub } from 'localsnake-sdk-web';
70
+
71
+ LocalSnake.onEvent((event) => {
72
+ console.log(event.type, event.payload);
73
+ });
74
+
75
+ await LocalSnake.init();
76
+ await LocalSnake.sendMessage('帮我查一下今天的安排');
77
+
78
+ const metadata = await LocalSnake.workspace.readJson('works/montage/knowledge/metadata.json');
79
+ const models = await LocalHub.llm.request('/v1/models');
80
+ ```
81
+
82
+ 默认导入返回 `LocalSnake`:
83
+
84
+ ```ts
85
+ import LocalSnake from 'localsnake-sdk-web';
86
+ ```
87
+
88
+ ## 生命周期
89
+
90
+ | API | 触发时机 | 回调参数 | 典型用途 |
91
+ | --- | --- | --- | --- |
92
+ | `LocalSnake.init(handler?)` | 页面 load 后完成 Host `app.init` 握手。若 WebView 已可见,初始化后会回放 `onShow()`。 | `LocalSnakeAppContext` | 读取当前 Agent、会话、工作区并初始化应用。 |
93
+ | `LocalSnake.onShow(handler)` | WebView 首次显示或从隐藏状态恢复。 | 无 | 恢复动画、渲染、轮询或可见状态同步。 |
94
+ | `LocalSnake.onHide(handler)` | WebView 隐藏。 | 无 | 暂停动画、渲染或高成本计算。 |
95
+ | `LocalSnake.onDestroy(handler)` | WebView 卸载、分离或切换应用 / Agent / 页面。 | 无 | 清理定时器、WebGL、音频和订阅。 |
96
+
97
+ 生命周期回调不会通过公开的 `LocalSnake.onEvent()` 投递。
98
+
99
+ ## 常用 API
100
+
101
+ | API | 返回值 | 说明 |
102
+ | --- | --- | --- |
103
+ | `LocalSnake.onEvent(handler)` | `() => void` | 订阅非生命周期实时 Host 事件。SDK 可能回放最新 `runtime_status`,避免应用错过初始状态。 |
104
+ | `LocalSnake.sendMessage(text, options?)` | `Promise<unknown>` | 向当前打开会话发送用户消息。传入 `options.sessionKey` 可指定目标会话。 |
105
+ | `LocalSnake.getHistory()` | 暂不支持 | 预留给未来显式历史 API。 |
106
+ | `LocalSnake.workspace.readText(path)` | `Promise<string \| null>` | 读取当前 Agent 工作区下的 UTF-8 文本文件。文件不存在返回 `null`。 |
107
+ | `LocalSnake.workspace.readJson<T>(path)` | `Promise<T \| null>` | 读取并解析当前 Agent 工作区下的 JSON 文件。文件不存在返回 `null`,JSON 非法则 reject。 |
108
+ | `LocalSnake.workspace.writeText(path, content)` | `Promise<{ path: string, success: boolean, error?: string }>` | 写入当前 Agent 工作区下的 UTF-8 文本文件。父目录由 Host 创建。 |
109
+ | `LocalSnake.workspace.writeJson(path, value)` | `Promise<{ path: string, success: boolean, error?: string }>` | 使用两个空格格式化 JSON 后写入当前 Agent 工作区。 |
110
+ | `LocalSnake.request(method, payload?)` | `Promise<T>` | 底层请求入口。常规场景优先使用封装 API。 |
111
+ | `LocalHub.request(pathOrRequest, init?)` | `Promise<LocalSnakeLocalHubResponse>` | 底层 LocalHub 代理入口。优先使用 `LocalHub.agent`、`LocalHub.llm`、`LocalHub.proxy`。 |
112
+
113
+ ## 发送消息
114
+
115
+ ```ts
116
+ await LocalSnake.sendMessage('Summarize this workspace');
117
+
118
+ await LocalSnake.sendMessage('Send this to a child session', {
119
+ sessionKey: 'session-key',
120
+ });
121
+ ```
122
+
123
+ 如果不传 `sessionKey`,LocalSnake 会把消息发送到当前激活会话。
124
+
125
+ ## 工作区文件
126
+
127
+ `LocalSnake.workspace` 的访问范围由 `init()` 返回的当前 Agent 工作区决定。路径必须是相对工作区路径,例如 `works/montage/knowledge/metadata.json`;Host 会拒绝绝对路径、空路径、空字节和 `..` 越界路径。
128
+
129
+ ```ts
130
+ const index = await LocalSnake.workspace.readJson('works/montage/knowledge/metadata.json');
131
+ await LocalSnake.workspace.writeJson('works/montage/knowledge/metadata.json', {
132
+ schema_version: 1,
133
+ kind: 'dreamer.knowledge.index',
134
+ updated_at: new Date().toISOString(),
135
+ projects: index?.projects ?? [],
136
+ });
137
+ ```
138
+
139
+ ## LocalHub
140
+
141
+ `LocalHub` 按 LocalHub 的三个公开路由域拆分:
142
+
143
+ | API | 路由前缀 | 用途 |
144
+ | --- | --- | --- |
145
+ | `LocalHub.agent.request(pathOrRequest, init?)` | `/agent/*` | LocalHub 管理、搜索桥接和 evolution API。 |
146
+ | `LocalHub.llm.request(pathOrRequest, init?)` | `/llm/*` | LLM 代理调用,包括 OpenAI 兼容和 Anthropic 兼容路由。 |
147
+ | `LocalHub.proxy.request(pathOrRequest, init?)` | `/proxy/*` | 调用 LocalHub 中配置的 HTTP 请求代理。 |
148
+ | `LocalHub.proxy.route(routes)` | `/agent/proxies` | 覆盖系统 LocalHub Key 的请求代理路由配置。传 `[]` 可清空。 |
149
+ | `LocalHub.proxy.websocket(pathOrRequest, options?)` | `/proxy/*` | 调用 LocalHub 中配置的 WebSocket 请求代理。 |
150
+
151
+ LocalSnake 会在后台提供 LocalHub base URL,并把已配置的 `localHubApiKey` 注入为 `Authorization: Bearer <key>`。应用不应该读取、保存或传递 `localHubApiKey`。
152
+
153
+ ```ts
154
+ await LocalHub.agent.request('/initialized');
155
+ await LocalHub.llm.request('/v1/models');
156
+
157
+ const res = await LocalHub.proxy.request('/orders/42', {
158
+ method: 'POST',
159
+ body: { status: 'paid' },
160
+ });
161
+ ```
162
+
163
+ 请求代理路由可以通过语义化 helper 替换。LocalHub 管理端点是 `/agent/proxies`,但 SDK 暴露在 `LocalHub.proxy.route()` 下,因为它配置的是运行时 `/proxy/*` 路由。
164
+
165
+ ```ts
166
+ await LocalHub.proxy.route([
167
+ {
168
+ name: 'orders',
169
+ protocol: 'http',
170
+ path: '/proxy/orders/*',
171
+ targetUrl: 'https://api.example.com/orders',
172
+ headers: { Authorization: 'Bearer upstream-token' },
173
+ },
174
+ ]);
175
+
176
+ await LocalHub.proxy.route([]);
177
+ ```
178
+
179
+ WebSocket 请求代理通过一个轻量的类浏览器对象暴露。它由 Host bridge 承载,因此 LocalSnake 可以在 LocalHub 握手阶段附加所需的 Authorization 头。
180
+
181
+ ```ts
182
+ const socket = LocalHub.proxy.websocket('/events');
183
+
184
+ socket.onopen = () => socket.send('hello');
185
+ socket.onmessage = (event) => {
186
+ console.log(event.data);
187
+ };
188
+ socket.onclose = (event) => {
189
+ console.log(event.code, event.reason);
190
+ };
191
+ ```
192
+
193
+ ## 实时事件契约
194
+
195
+ `LocalSnake.onEvent()` 接收单个事件对象:
196
+
197
+ | 字段 | 类型 | 说明 |
198
+ | --- | --- | --- |
199
+ | `type` | `string` | 事件类型。 |
200
+ | `timestamp` | `number` | Host 侧毫秒时间戳。 |
201
+ | `payload` | `object` | 事件载荷,结构取决于 `type`。 |
202
+
203
+ `LocalSnake.onEvent()` 事件是自包含的,不包含 `LocalSnakeAppContext`;Agent、会话和工作区信息请从 `init()` 读取。
204
+
205
+ | 事件类型 | 来源 | Payload | 说明 |
206
+ | --- | --- | --- | --- |
207
+ | `user_message` | OpenClaw session | `{ message: LocalSnakeMessage }` | 当前打开会话收到实时用户消息。 |
208
+ | `assistant_delta` | OpenClaw stream | `{ text: string, message?: LocalSnakeMessage, runId?: string \| null }` | 实时 assistant 文本增量。只追加到当前 message / run。 |
209
+ | `assistant_message` | OpenClaw session | `{ message: LocalSnakeMessage }` | assistant 最终消息。 |
210
+ | `tool_call` | OpenClaw tool call | `{ toolCallId?: string, toolName?: string, input?: unknown, status?: "running" \| "completed" \| "error" }` | 工具调用事件,不应拼进 assistant 文本。 |
211
+ | `tool_result` | OpenClaw tool result | `{ toolCallId?: string, toolName?: string, result?: unknown, text?: string, isError?: boolean }` | 工具响应 / 结果事件。流式工具输出可能产生多个结果事件。 |
212
+ | `send_message` | SDK / AppManager | `{ text: string, sessionKey?: string \| null }` | 嵌入应用通过 `LocalSnake.sendMessage()` 提交用户消息。 |
213
+ | `runtime_status` | AppManager | `LocalSnakeRuntimeStatus` | 当前会话运行状态。建议优先用它做场景 / 状态映射。 |
214
+
215
+ ## 类型说明
216
+
217
+ ### `LocalSnakeAppContext`
218
+
219
+ | 字段 | 类型 | 说明 |
220
+ | --- | --- | --- |
221
+ | `app` | `"LocalSnake"` | Host 应用名。 |
222
+ | `platform` | `"desktop" \| "mobile" \| "unknown"` | Host 平台。 |
223
+ | `bridge` | `"electron-webview" \| "ios-webkit" \| "android-webview" \| "react-native-webview" \| "webview2" \| "post-message" \| "unknown"` | JS bridge 类型。 |
224
+ | `agentId` | `string \| null` | 当前 Agent id。 |
225
+ | `sessionKey` | `string \| null` | 当前打开会话 key。 |
226
+ | `workspacePath` | `string \| null` | 当前 Agent 工作区路径。 |
227
+ | `visibilityState` | `"visible" \| "hidden"` | init 时 WebView 可见状态。 |
228
+ | `capabilities.chat` | `boolean` | 是否支持 `sendMessage`。 |
229
+ | `capabilities.history` | `boolean` | 当前为 `false`;SDK Web 暂不暴露历史。 |
230
+ | `capabilities.localHub` | `boolean` | 是否支持 LocalHub 代理 API。 |
231
+ | `capabilities.workspace` | `boolean` | 是否支持当前 Agent 工作区文件 API。 |
232
+ | `capabilities.events` | `string[]` | Host 可能通过 `onEvent()` 发出的非生命周期事件类型。 |
233
+
234
+ ### `LocalSnakeRuntimeStatus`
235
+
236
+ | 字段 | 类型 | 说明 |
237
+ | --- | --- | --- |
238
+ | `status` | `"idle" \| "working" \| "subagent"` | 粗粒度运行状态。普通主进程工具调用保持 `working`;`subagent` 表示子 Agent 协调中。 |
239
+ | `activityType` | `"idle" \| "message" \| "tool" \| "subagent" \| "queued" \| "thinking"` | 更具体的活动类型,适合 UI 状态映射。 |
240
+ | `sessionKey` | `string \| null \| undefined` | 当前会话 key。 |
241
+ | `activeRunId` | `string \| null \| undefined` | 已知的当前 runtime run id。 |
242
+ | `sending` | `boolean` | 当前会话是否被认为正在运行。 |
243
+ | `pendingFinal` | `boolean` | Host 是否正在等待最终 assistant 响应。 |
244
+ | `queuedCount` | `number` | 排队中的 steer / follow-up 消息数量。 |
245
+ | `runningTools` | `Array<{ toolCallId?: string, toolName?: string, status?: "running" \| "completed" \| "error" }>` | Host 已知的运行中工具。 |
246
+ | `subagentCount` | `number?` | 被识别为 subagent / delegate / spawn 的运行中工具数量。 |
247
+ | `reason` | `string` | 诊断原因,例如 `idle`、`main-session-active`、`session-run-active` 或 `subagent-tools-running`。 |
248
+
249
+ ### `LocalSnakeMessage`
250
+
251
+ | 字段 | 类型 | 说明 |
252
+ | --- | --- | --- |
253
+ | `id` | `string?` | 已知的消息 id。 |
254
+ | `role` | `"user" \| "assistant" \| "system" \| "toolResult" \| "tool"` | 标准化消息角色。 |
255
+ | `content` | `unknown` | Host / runtime 提供的消息内容。 |
256
+ | `timestamp` | `number?` | 已知的消息时间戳。 |
257
+ | `model` | `string?` | 已知的模型 id。 |
258
+ | `provider` | `string?` | 已知的 Provider 名称。 |
259
+ | `toolCallId` | `string?` | 已知的工具调用 id。 |
260
+ | `toolName` | `string?` | 已知的工具名称。 |
261
+ | `isError` | `boolean?` | 是否为错误结果。 |
262
+ | `usage` | `unknown` | 已知的 token usage 或其它 runtime 元数据。 |
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const k="localsnake.sdk-web",M=1,U=2e3;function B(){return typeof window>"u"?null:window}function j(){const n=typeof crypto<"u"&&"randomUUID"in crypto?crypto.randomUUID():Math.random().toString(36).slice(2);return`sdkweb_${Date.now().toString(36)}_${n}`}function V(n){return!!(n&&typeof n=="object"&&n.channel===k&&n.protocol===M)}function q(n){if(typeof n=="string")try{return q(JSON.parse(n))}catch{return null}return V(n)?n:null}function J(n){return n?n.LocalSnakeAppManager?.postMessage?n.LocalSnakeAppManager.platform==="electron-webview"?"electron-webview":"android-webview":n.webkit?.messageHandlers?.LocalSnakeAppManager?.postMessage?"ios-webkit":n.ReactNativeWebView?.postMessage?"react-native-webview":n.chrome?.webview?.postMessage?"webview2":n.parent&&n.parent!==n?"post-message":"unknown":"unknown"}class m{constructor(){this.pending=new Map,this.eventHandlers=new Set,this.recentEventKeys=new Map,this.handleWindowMessage=e=>{this.handleIncoming(e.data)},this.handleCustomMessage=e=>{this.handleIncoming(e.detail)},this.handleWebView2Message=e=>{this.handleIncoming(e.data)},this.win=B(),this.platform=J(this.win),this.attachListeners()}dispose(){if(this.win){this.win.removeEventListener("message",this.handleWindowMessage),this.win.removeEventListener("localsnake-app-manager-message",this.handleCustomMessage),this.win.chrome?.webview?.removeEventListener?.("message",this.handleWebView2Message);for(const e of this.pending.values())clearTimeout(e.timeout),e.retry&&clearInterval(e.retry),e.reject(new Error("LocalSnake transport disposed"));this.pending.clear(),this.eventHandlers.clear(),this.recentEventKeys.clear()}}onEvent(e){return this.eventHandlers.add(e),()=>this.eventHandlers.delete(e)}request(e,t,r=3e4){const o=j(),a={channel:k,protocol:M,id:o,type:"request",method:e,payload:t};return new Promise((u,h)=>{const c=setTimeout(()=>{const l=this.pending.get(o);l?.retry&&clearInterval(l.retry),this.pending.delete(o),h(new Error(`LocalSnake AppManager request timed out: ${e}`))},r),d=e==="app.init"?setInterval(()=>{if(this.pending.has(o))try{this.post(a)}catch{}},800):void 0;this.pending.set(o,{resolve:l=>u(l),reject:h,timeout:c,retry:d});try{this.post(a)}catch(l){clearTimeout(c),d&&clearInterval(d),this.pending.delete(o),h(l)}})}attachListeners(){this.win&&(this.win.addEventListener("message",this.handleWindowMessage),this.win.addEventListener("localsnake-app-manager-message",this.handleCustomMessage),this.win.chrome?.webview?.addEventListener?.("message",this.handleWebView2Message))}post(e){const t=this.win;if(!t)throw new Error("LocalSnake SDK Web requires a browser window");if(t.LocalSnakeAppManager?.postMessage){t.LocalSnakeAppManager.postMessage(e);return}if(t.webkit?.messageHandlers?.LocalSnakeAppManager?.postMessage){t.webkit.messageHandlers.LocalSnakeAppManager.postMessage(e);return}if(t.ReactNativeWebView?.postMessage){t.ReactNativeWebView.postMessage(JSON.stringify(e));return}if(t.chrome?.webview?.postMessage){t.chrome.webview.postMessage(e);return}if(t.parent&&t.parent!==t){t.parent.postMessage(e,"*");return}throw new Error("LocalSnake AppManager bridge is not available")}handleIncoming(e){const t=q(e);if(t){if(t.type==="response"){this.handleResponse(t);return}t.type==="event"&&(t.event.type==="localhub.websocket"||!this.isDuplicateEvent(t))&&this.handleEvent(t)}}isDuplicateEvent(e){const t=Date.now();for(const[o,a]of this.recentEventKeys)t-a>U&&this.recentEventKeys.delete(o);const r=JSON.stringify(e.event);return this.recentEventKeys.has(r)?!0:(this.recentEventKeys.set(r,t),!1)}handleResponse(e){const t=this.pending.get(e.id);if(!t)return;if(clearTimeout(t.timeout),t.retry&&clearInterval(t.retry),this.pending.delete(e.id),e.ok){t.resolve(e.result);return}const r=e.error?.message||"LocalSnake AppManager request failed",o=new Error(r);e.error?.code&&Object.defineProperty(o,"code",{value:e.error.code,enumerable:!0}),t.reject(o)}handleEvent(e){for(const t of[...this.eventHandlers])try{t(e.event)}catch{}}}const x=0,E=1,L=2,y=3;function A(n,e){return typeof n=="string"?{path:n,...e}:{...n,...e,path:n.path}}function z(n,e){const t=`/${n}`,r=e.trim();return r?r===t||r.startsWith(`${t}/`)?r:`${t}${r.startsWith("/")?r:`/${r}`}`:t}function O(n,e,t){const r=A(e,t);return{...r,path:z(n,r.path)}}function f(n){return typeof n=="string"&&n.trim()?n.trim():void 0}function G(n){if(!n||typeof n!="object"||Array.isArray(n))return;const e={};for(const[t,r]of Object.entries(n)){if(typeof r!="string")continue;const o=t.trim();o&&(e[o]=r.trim())}return Object.keys(e).length>0?e:void 0}function T(n,e){if(!n||typeof n!="object"||Array.isArray(n))throw new Error(`LocalHub proxy route at index ${e} must be an object`);const t=n,r=f(t.name),o=f(t.path),a=f(t.targetUrl);if(!r)throw new Error(`LocalHub proxy route at index ${e} requires name`);if(!o)throw new Error(`LocalHub proxy route at index ${e} requires path`);if(!a)throw new Error(`LocalHub proxy route at index ${e} requires targetUrl`);const u=f(t.protocol);if(u&&u!=="http"&&u!=="websocket")throw new Error(`LocalHub proxy route at index ${e} protocol must be http or websocket`);const h=u,c=f(t.id),d=f(t.targetPath),l=G(t.headers);return{...c?{id:c}:{},...h?{protocol:h}:{},name:r,path:o,targetUrl:a,...d?{targetPath:d}:{},...l?{headers:l}:{}}}function F(n){if(!Array.isArray(n))throw new Error("LocalHub.proxy.route expects a routes array");return n.map((e,t)=>T(e,t))}function N(n){return n.json&&typeof n.json=="object"?n.json:{}}function Q(n){const e=N(n).data;if(!e||typeof e!="object"||Array.isArray(e))return[];const t=e.proxies;return Array.isArray(t)?t.map((r,o)=>T(r,o)):[]}function X(n){const e=N(n),t=typeof e.msg=="string"?e.msg:void 0,r=n.ok&&(e.code===void 0||e.code===0);return{ok:r,status:n.status,routes:r?Q(n):[],...t?{message:t}:{},...r?{}:{error:n.error||t||`HTTP ${n.status}`}}}function Y(){const n=typeof crypto<"u"&&"randomUUID"in crypto?crypto.randomUUID():Math.random().toString(36).slice(2);return`localhub_ws_${Date.now().toString(36)}_${n}`}function Z(n){if(typeof n=="string")return{data:n,binary:!1,encoding:"text"};const e=n instanceof Uint8Array?n:new Uint8Array(n);let t="";for(let r=0;r<e.length;r+=1)t+=String.fromCharCode(e[r]);return{data:btoa(t),binary:!0,encoding:"base64"}}function ee(n){if(!n.binary)return n.data;const e=atob(n.data),t=new Uint8Array(e.length);for(let r=0;r<e.length;r+=1)t[r]=e.charCodeAt(r);return t.buffer}class te{constructor(e,t,r){this.transport=e,this.socketId=Y(),this.CONNECTING=x,this.OPEN=E,this.CLOSING=L,this.CLOSED=y,this.listeners={open:new Set,message:new Set,error:new Set,close:new Set},this.unsubscribe=null,this.state=x,this.activeProtocol="",this.onopen=null,this.onmessage=null,this.onerror=null,this.onclose=null,this.unsubscribe=this.transport.onEvent(o=>{o.type==="localhub.websocket"&&o.payload.socketId===this.socketId&&this.handleBridgeEvent(o.payload)}),this.transport.request("localhub.websocket.open",{socketId:this.socketId,request:t,options:r}).catch(o=>{const a=o instanceof Error?o.message:String(o);this.emit("error",{type:"error",error:a}),this.finishClose(1006,a,!1)})}get readyState(){return this.state}get protocol(){return this.activeProtocol}addEventListener(e,t){this.listeners[e].add(t)}removeEventListener(e,t){this.listeners[e].delete(t)}async send(e){if(this.state!==E)throw new Error("LocalHub WebSocket is not open");await this.transport.request("localhub.websocket.send",{socketId:this.socketId,...Z(e)})}async close(e=1e3,t=""){this.state===L||this.state===y||(this.state=L,await this.transport.request("localhub.websocket.close",{socketId:this.socketId,code:e,reason:t}))}handleBridgeEvent(e){if(e.event==="open"){this.state=E,this.activeProtocol=e.protocol??"",this.emit("open",{type:"open",protocol:this.activeProtocol});return}if(e.event==="message"){this.emit("message",{type:"message",data:ee(e)});return}if(e.event==="error"){this.emit("error",{type:"error",error:e.error});return}this.finishClose(e.code,e.reason,e.wasClean)}finishClose(e,t,r){this.state!==y&&(this.state=y,this.unsubscribe?.(),this.unsubscribe=null,this.emit("close",{type:"close",code:e,reason:t,wasClean:r}))}emit(e,t){e==="open"&&this.onopen?.(t),e==="message"&&this.onmessage?.(t),e==="error"&&this.onerror?.(t),e==="close"&&this.onclose?.(t);for(const r of[...this.listeners[e]])r(t)}}class C{constructor(e,t){this.transport=e,this.domain=t}request(e,t){return this.transport.request("localhub.request",O(this.domain,e,t))}}class ne extends C{constructor(e){super(e,"agent")}}class re extends C{constructor(e){super(e,"proxy")}async route(e){const t=await this.transport.request("localhub.request",{path:"/agent/proxies",method:"PUT",body:{proxies:F(e)}});return X(t)}websocket(e,t){const r=O("proxy",e);return new te(this.transport,r,t)}}class b{constructor(e){this.transport=e,this.agent=new ne(e),this.llm=new C(e,"llm"),this.proxy=new re(e)}request(e,t){return this.transport.request("localhub.request",A(e,t))}}function se(n=new m){return new b(n)}const oe=100;function P(){return typeof document>"u"||document.readyState==="complete"?Promise.resolve():new Promise(n=>{if(typeof window<"u"){window.addEventListener("load",()=>n(),{once:!0});return}const e=()=>{document.readyState==="complete"&&(document.removeEventListener("readystatechange",e),n())};document.addEventListener("readystatechange",e)})}function p(n){Promise.resolve(n()).catch(()=>{})}function ie(n){const{context:e,...t}=n;return t}function W(n={}){const e=n.transport??new m,t=n.LocalHub??new b(e),r=new Set,o={show:new Set,hide:new Set,destroy:new Set},a=[];let u=null,h=null,c=null,d=!1,l=null;const D=()=>{a.length=0,u=null,h=null,c=null,d=!1},R=s=>{a.push(s),a.length>oe&&a.shift()},S=s=>{if(s.phase==="show"){if(c===!0)return;c=!0;for(const i of[...o.show])p(i);return}if(s.phase==="hide"){if(c===!1)return;c=!1;for(const i of[...o.hide])p(i);return}if(s.phase==="destroy"){if(l)return;l=s;for(const i of[...o.destroy])p(i);D();return}},$=s=>{if(!(d||!(s.visibilityState==="visible"||s.visibilityState==null&&c!==!1))){d=!0,c=!0;for(const g of[...o.show])p(g)}};e.onEvent(s=>{if(s.type==="lifecycle"){S(s.payload);return}if(s.type==="localhub.websocket")return;const i=ie(s);R(i);for(const g of[...r])try{g(i)}catch{}});const K=()=>(u||(u=P().then(()=>e.request("app.init",{sdkPlatform:e.platform})).then(s=>(h=s,s))),u),v=(s,i)=>(o[s].add(i),(s==="show"&&c===!0||s==="hide"&&c===!1||s==="destroy"&&l)&&p(i),()=>o[s].delete(i));return typeof window<"u"&&(window.addEventListener("pagehide",()=>{S({phase:"destroy",reason:"pagehide"})}),window.addEventListener("beforeunload",()=>{S({phase:"destroy",reason:"beforeunload"})})),{LocalHub:t,workspace:{readText:async s=>{const i=await e.request("workspace.readText",{path:s});return typeof i.content=="string"?i.content:null},readJson:async s=>{const i=await e.request("workspace.readText",{path:s});return typeof i.content!="string"?null:JSON.parse(i.content)},writeText:(s,i)=>e.request("workspace.writeText",{path:s,content:i}),writeJson:(s,i)=>e.request("workspace.writeText",{path:s,content:`${JSON.stringify(i,null,2)}
2
+ `})},init:async s=>{await P();const i=h??await K();return s&&await s(i),$(i),i},onShow:s=>v("show",s),onHide:s=>v("hide",s),onDestroy:s=>v("destroy",s),onEvent:s=>{r.add(s);for(const i of a)try{s(i)}catch{}return()=>r.delete(s)},sendMessage:(s,i)=>e.request("chat.sendMessage",{text:s,options:i}),getHistory:()=>e.request("chat.getHistory"),request:(s,i)=>e.request(s,i)}}const _=new m,H=new b(_),I=W({transport:_,LocalHub:H}),w=typeof window<"u"?window:void 0;w&&(w.LocalSnake||(w.LocalSnake=I),w.LocalHub||(w.LocalHub=H));exports.LocalHub=H;exports.LocalSnake=I;exports.LocalSnakeLocalHubClient=b;exports.LocalSnakeTransport=m;exports.SDK_WEB_CHANNEL=k;exports.SDK_WEB_PROTOCOL_VERSION=M;exports.createLocalHubClient=se;exports.createLocalSnakeClient=W;exports.default=I;