lsagent-sdk 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LSAgent Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,316 @@
1
+ # lsagent-sdk — 让你的网页使用用户本机的 AI
2
+
3
+ > 浏览器 JS SDK。你的网页通过它连接跑在用户自己电脑上的 AI 运行时
4
+ > [lsagent](https://www.npmjs.com/package/lsagent):用户在页面上传提单 / 发票 / 客户消息,
5
+ > AI 在用户本机解析成结构化字段回填表单;也能让 AI 读用户的邮箱。
6
+ > 业务系统**零后端改造**——文件不用传到服务器,模型密钥不用你代管。
7
+
8
+ ```text
9
+ 你的网页 ── lsagent-sdk(WebSocket,首次系统确认一次)──> 用户电脑上的 lsagent ──> 模型 API / 本机文件 / 邮箱
10
+
11
+ 只监听 ws://127.0.0.1:37700/ws
12
+ ```
13
+
14
+ 典型场景:补料 / 单证 AI 导入、客户消息抽取建单、发票识别、邮件内容提取等——
15
+ 一切"AI 处理用户自己电脑上的东西"的需求。
16
+
17
+ ## 与 lsagent 配合使用
18
+
19
+ [lsagent](https://www.npmjs.com/package/lsagent)(npm 包)与 lsagent-sdk 是一对,各装一端:
20
+
21
+ | 端 | 安装 | 角色 |
22
+ | --- | --- | --- |
23
+ | **用户的电脑** | `npm i -g lsagent`,运行 `lsagent` | AI 运行时:连模型 API、读本机文件、收发邮件、执行任务;只监听 `ws://127.0.0.1:37700/ws` |
24
+ | **你的网页** | `npm i lsagent-sdk` | 遥控器:WebSocket 连接 + 认证 + 任务/事件 API |
25
+
26
+ **用户侧一次性准备**(你的页面可以引导完成):
27
+
28
+ 1. 安装 [Node.js](https://nodejs.org) ≥ 20 → `npm i -g lsagent` → 终端运行 `lsagent`(前台常驻;开机自启自行加系统启动项)。
29
+ 2. **无需配对码、无需配白名单**:网页不带配对码连接时,用户电脑上会弹一次系统确认框,
30
+ 点"是"后你的网站即持久授权、以后无感直连(用户可用 `lsagent --disallow-origin` 撤销)。
31
+ localhost 开发环境同样只弹一次。
32
+ 3. 模型配置(API key + provider):用户手改两个 JSON 文件(格式见 lsagent README),
33
+ **或完全由你的网页引导完成**(见下方「第 1 层」的 `saveProvider()`,推荐)。
34
+
35
+ **连接握手时序**(免码模式):
36
+
37
+ ```text
38
+ connect() ──> connecting ──> pending(用户电脑弹系统确认框,onState 可感知)
39
+ ──用户点「是」──> authed(此后刷新页面 / 重启浏览器无感直连)
40
+ ```
41
+
42
+ **lsagent 没在运行的判别**:`waitForAuthed()` 超时返回 `false` 且 `getLastError()` 含
43
+ `code=1006`(连不上)——此时页面应出安装引导(指到
44
+ [lsagent README](https://www.npmjs.com/package/lsagent)),用户装好后再 `connect()` 重试。
45
+
46
+ **兼容性**:免码确认制需 lsagent ≥ 0.1.2(更早版本会按配对码错误拒绝免码连接);
47
+ 配对码模式任意版本可用。`npm i -g lsagent` 默认装最新版,一般无需关心。
48
+
49
+ **授权管理命令**(在用户电脑执行,可写进你的用户帮助文档):
50
+
51
+ ```bash
52
+ lsagent --show-code # 查看 6 位配对码(配对码模式用)
53
+ lsagent --list-origins # 查看白名单与确认授权
54
+ lsagent --allow-origin https://crm.example.com # 手动放行你的网站(免弹窗)
55
+ lsagent --disallow-origin https://crm.example.com # 撤销授权(下次连接重新弹窗确认)
56
+ ```
57
+
58
+ ## 安装
59
+
60
+ **方式一:script 标签**(零构建;从本包拷 `dist/lsagent-sdk.js` 到你的 public/,或用 CDN)
61
+
62
+ ```html
63
+ <script src="lsagent-sdk.js"></script>
64
+ <!-- CDN:<script src="https://unpkg.com/lsagent-sdk/dist/lsagent-sdk.js"></script> -->
65
+ <script>
66
+ const client = new LsagentSDK.LsagentClient(); // 免码模式:首次弹系统确认,一次点击永久授权
67
+ </script>
68
+ ```
69
+
70
+ **方式二:npm / ESM**
71
+
72
+ ```bash
73
+ npm i lsagent-sdk
74
+ ```
75
+
76
+ ```ts
77
+ import { LsagentClient } from "lsagent-sdk";
78
+ ```
79
+
80
+ ## 快速上手
81
+
82
+ ```ts
83
+ const client = new LsagentClient(); // 免码模式(推荐):首次连接弹系统确认框,用户点一次"是"即永久授权
84
+
85
+ client.onState((s) => {
86
+ console.log("连接状态:", s); // disconnected|connecting|connected|pending|authed
87
+ if (s === "pending") showHint("请在弹出的系统确认框上点【是】");
88
+ });
89
+ client.connect();
90
+ await client.waitForAuthed(120_000); // 首次要留时间等用户点确认;false = lsagent 未运行
91
+
92
+ // 创建并执行一个任务(firstMessage 立即开始;模型建议显式指定,见「第 1 层」)
93
+ const task = await client.createTask({
94
+ title: "整理今日邮件摘要",
95
+ provider: "deepseek", modelId: "deepseek-chat", // 用户已配置的模型
96
+ firstMessage: "把今天收到的邮件整理成摘要表格发给我",
97
+ systemPromptAppend: "输出使用简体中文。", // 可选:业务背景/输出约定
98
+ mcpServers: ["crm-tools"], // 可选:只接入业务系统自己的 MCP 工具
99
+ });
100
+
101
+ // 实时进度(事件全量广播,按 taskId 过滤)
102
+ client.onEvent((event, payload) => {
103
+ if (payload.taskId !== task.id) return;
104
+ if (event === "task.delta") appendStream(payload.text); // 流式文本
105
+ if (event === "task.status") onDone(payload.status); // idle|running|error
106
+ });
107
+
108
+ // 追问 / 控制
109
+ await client.prompt(task.id, "再按发件人分组");
110
+ await client.steer(task.id, "改成按主题分组"); // 执行中插入转向
111
+ await client.abort(task.id); // 中止
112
+
113
+ // 文件:先落到用户本机,AI 才能读(≤20MB)
114
+ const buf = await file.arrayBuffer();
115
+ const { path } = await client.request("fs.saveUpload", {
116
+ name: file.name,
117
+ bytesBase64: arrayBufferToBase64(buf), // FileReader.readAsDataURL 后取逗号后一段也行
118
+ });
119
+
120
+ // 查询
121
+ const models = await client.listModels();
122
+ const messages = await client.getMessages(task.id);
123
+ ```
124
+
125
+ 完整可跑示例见 `demo/`(`npm run demo` 后开 http://127.0.0.1:4567)。
126
+
127
+ ## API
128
+
129
+ | 成员 | 说明 |
130
+ | --- | --- |
131
+ | `new LsagentClient({ url?, pairingCode? })` | `url` 缺省 `ws://127.0.0.1:37700/ws`,远程模式填 `ws://IP:37700/ws`;**不传 `pairingCode` = 免码模式**(推荐,首次系统弹窗确认一次),传则走配对码模式(要求页面是 localhost 或在白名单) |
132
+ | `connect()` / `disconnect()` | 连接(幂等,断线自动指数退避重连)/ 主动断开 |
133
+ | `getState()` / `onState(cb)` | 状态机:`disconnected → connecting → connected → (pending) → authed`;`pending` = 等用户在本机点确认框 |
134
+ | `getLastError()` | 最近一次失败原因(如"等待用户在本机确认接入…"、"接入被拒绝(denied)"),UI 提示用 |
135
+ | `waitForAuthed(timeoutMs?)` | 等待配对完成;超时返回 `false`。免码模式首次等待用户点击,建议给足超时(如 120_000) |
136
+ | `onEvent(cb)` | 订阅事件(见下表),返回取消函数 |
137
+ | `request(type, payload?, timeoutMs?)` | 通用透传:lsagent WS 协议全开放(`fs.*`、`email.*` 等) |
138
+ | `createTask(opts?)` | 建任务;`firstMessage` 非空则立即执行 |
139
+ | `prompt(taskId, text)` | 追加消息(任务空闲时触发执行;执行中会被拒,改用 `steer` 或等待) |
140
+ | `steer(taskId, text)` / `abort(taskId)` | 执行中转向 / 中止 |
141
+ | `listModels()` / `listTasks()` / `getMessages(taskId)` | 查询模型 / 任务列表 / 任务消息 |
142
+ | `listProviders()` / `saveProvider(spec)` | 探测本机模型服务配置状态(`hasKey`)/ 新增更新模型服务(见「第 1 层」) |
143
+
144
+ `createTask` 选项:`title`、`provider`+`modelId`(缺省用全局默认模型)、`cwd`(本机工作目录)、
145
+ `firstMessage`、`systemPromptAppend`、`skills`、`mcpServers`(详见下节)。
146
+
147
+ ## 模型、提示词与能力配置(集成方必读)
148
+
149
+ 配置分三层,**全部经 SDK 同一条连接传输**(`createTask` 参数 + `request()` 透传),无需额外通道。
150
+
151
+ ### 第 1 层:模型——密钥在用户侧,选择在任务级
152
+
153
+ | 关注点 | 谁负责 | 怎么做 |
154
+ | --- | --- | --- |
155
+ | 模型服务与 API 密钥 | **用户侧** | 配置存用户本机 `~/.lsagent/pi/`(`models.json` + `auth.json`);业务系统可探测、可代填,但不该后端囤 key |
156
+ | 用哪个模型跑任务 | **业务系统**(任务级指定) | `createTask({ provider, modelId })`;建议**总是显式指定**(见下方「默认模型」注意) |
157
+
158
+ ```ts
159
+ const models = await client.listModels(); // available=true 的才可指定(已配凭据)
160
+ const task = await client.createTask({
161
+ provider: models[0].provider, modelId: models[0].id,
162
+ firstMessage: "...",
163
+ });
164
+ ```
165
+
166
+ 指定了用户未配置的模型会报错「模型不存在或已删除」。
167
+
168
+ **首次配置引导**(用户刚装 lsagent、还没配 key 时,全部经 SDK 同一条连接完成):
169
+
170
+ ```ts
171
+ // 1. 探测:没有任何已配 key 的服务 → 页面出引导表单
172
+ const providers = await client.listProviders();
173
+ const unconfigured = providers.length === 0 || providers.every((p) => !p.hasKey);
174
+
175
+ // 2. 用户在表单里选常见服务商预设或自填,提交时落到用户本机,保存即生效:
176
+ await client.saveProvider({
177
+ name: "deepseek", // provider 标识(自填时自定义,如 "my-company")
178
+ displayName: "DeepSeek",
179
+ baseUrl: "https://api.deepseek.com/v1",
180
+ apiType: "openai-completions", // OpenAI 兼容端点用这个;Anthropic 兼容用 "anthropic-messages"
181
+ apiKey: userEnteredKey, // 写入用户本机 auth.json;留空 = 保留原 key
182
+ models: [
183
+ { id: "deepseek-chat", name: "DeepSeek-V3", contextWindow: 131072, maxTokens: 8192 },
184
+ ],
185
+ });
186
+
187
+ // 3. 复查可用性,然后显式指定模型建任务
188
+ const models = await client.listModels();
189
+ ```
190
+
191
+ 常见预设速查(baseUrl + apiType):DeepSeek `https://api.deepseek.com/v1` / `openai-completions`;
192
+ Kimi `https://api.moonshot.cn/v1` / `openai-completions`;智谱 GLM `https://open.bigmodel.cn/api/anthropic` /
193
+ `anthropic-messages`。模型 `contextWindow`/`maxTokens` 填服务商标称值即可,影响上下文管理。
194
+
195
+ **企业统一密钥(可选)**:业务后端托管一份公司级 provider 配置,用户配对完成后页面从业务后端
196
+ 拉取并 `saveProvider` 推到用户本机——用户零配置。key 会落到用户本机明文文件(与用户自己配等价),
197
+ 是否采用由业务方自行评估。
198
+
199
+ **「默认模型」注意**:全局默认模型存在用户本机 `settings.json`,新装用户大概率没设——
200
+ 所以 **SDK 建任务时总是显式传 `provider` + `modelId`**,不要依赖缺省。
201
+
202
+ ### 第 2 层:提示词——systemPromptAppend 是业务注入的唯一正道
203
+
204
+ `createTask({ systemPromptAppend })` 把文本**追加**到系统提示之后(不替换内置工具指引),适合放:
205
+ 业务背景("这是货代公司的订单审核系统")、角色约定、输出格式("结果以 JSON 返回,字段…")、
206
+ 红线("不要执行删除类操作")。
207
+
208
+ ```ts
209
+ const task = await client.createTask({
210
+ systemPromptAppend: [
211
+ "你是订单审核助手,操作对象是本公司的 ERP。",
212
+ "输出固定 JSON:{approved: boolean, reason: string}。",
213
+ ].join("\n"),
214
+ firstMessage: orderText,
215
+ });
216
+ ```
217
+
218
+ 单次业务数据(订单内容等)放 `firstMessage` / `prompt()`,不要塞进系统提示。
219
+
220
+ ### 第 3 层:能力——Skills 与 MCP 工具
221
+
222
+ 任务通过参数引用能力,能力本体落在用户本机配置文件里(用户可见、可删,透明可控):
223
+
224
+ **Skills(给 AI 的操作手册/站点策略)**
225
+
226
+ ```ts
227
+ // 探测用户已有的
228
+ const local = await client.request("skills.local.list");
229
+
230
+ // 推送业务系统自己的 skill(首次集成时一次即可;内容 = markdown 手册)
231
+ await client.request("skills.local.save", {
232
+ name: "erp-order-guide",
233
+ content: "# ERP 订单审核指南\n1. 先查 ...\n2. ...",
234
+ origin: "业务系统名", // 可选,标记来源方便用户辨认
235
+ });
236
+
237
+ // 任务引用(按名内联全文进系统提示)
238
+ const task = await client.createTask({ skills: ["erp-order-guide"], firstMessage: "..." });
239
+ ```
240
+
241
+ **MCP(业务系统提供给 AI 的工具,如查订单/改状态)**
242
+
243
+ ```ts
244
+ // 注册业务系统自己的 MCP Server(http 型最简单:一个 URL + 鉴权头)
245
+ await client.request("mcp.local.save", {
246
+ server: {
247
+ name: "crm-tools",
248
+ type: "http",
249
+ url: "https://crm.example.com/mcp",
250
+ headers: { Authorization: "Bearer <用户级token>" },
251
+ enabled: true,
252
+ },
253
+ origin: "业务系统名",
254
+ });
255
+
256
+ // 任务只接入指定 MCP(缺省接入用户已启用的全部)
257
+ const task = await client.createTask({ mcpServers: ["crm-tools"], firstMessage: "..." });
258
+ ```
259
+
260
+ stdio 型(`command`/`args`/`env`)也支持,但会启动用户本机进程,优先用 http 型。
261
+
262
+ ### 推荐的首次集成初始化流程
263
+
264
+ ```
265
+ 用户打开业务页面 → SDK 免码连接(onState 出现 pending 时提示用户点系统确认框,waitForAuthed 等待)
266
+ → request("skills.local.list") / request("mcp.local.list") 探测本系统已注册的能力
267
+ → 缺则推送(skills.local.save + mcp.local.save;失败不阻塞,降级为纯 systemPromptAppend)
268
+ → 之后每次 createTask({ systemPromptAppend, skills, mcpServers, ... })
269
+ ```
270
+
271
+ 能力推送是**写用户本机配置**的操作,请在页面里明示,不要静默反复写入;
272
+ 版本更新时先比对内容再决定是否覆盖。
273
+
274
+ ## 事件(`onEvent`)
275
+
276
+ | 事件 | 载荷(均含 `taskId`) |
277
+ | --- | --- |
278
+ | `task.status` | `status: idle\|running\|error`,`error?` 错误描述 |
279
+ | `task.delta` | `text` 流式增量文本 |
280
+ | `task.message` | `message` 完整消息(role/text/usage…) |
281
+ | `task.tool` | `phase: start\|end`、`name` 工具名 |
282
+ | `task.usage` | `input`/`output`/`cost` 本轮用量 |
283
+ | `task.deleted` | 任务被删除 |
284
+ | `mail.new` | 新邮件(无 taskId) |
285
+
286
+ 事件为全量广播(与本机其它已配对客户端一致),业务方按 `taskId`/事件名自行过滤。
287
+
288
+ ## 安全模型
289
+
290
+ - **免码模式 = 系统弹窗确认制**:首次连接时用户电脑弹原生确认框,点一次"是"授权即持久
291
+ (存 `~/.lsagent/web-grants.json`,用户可 `lsagent --disallow-origin <url>` 撤销)。
292
+ 授权绑定来源 origin,浏览器无法伪造;点"否"只拒当次,同一网站连拒 3 次转静默拦截,
293
+ 恶意网站无法靠反复请求轰炸用户(10 分钟内最多弹 5 次确认框)。
294
+ - **配对码 = 完全授权**:持有配对码的页面可读本机文件、发邮件、执行任务,且要求页面
295
+ 来源是 localhost 或用户白名单(防钓鱼页骗码直连)。只在用户明确信任的系统中使用,
296
+ 建议用户输入而非硬编码进代码。
297
+ - lsagent 有防爆破:同 IP 连续错 5 次锁 10 分钟;配对码 6 位、服务只监听本机回环(远程模式除外)。
298
+
299
+ ## 限制
300
+
301
+ - 需用户电脑开机且 lsagent 在线(`onState` 可感知断线,SDK 自动重连)。
302
+ - 来源白名单为精确匹配,暂不支持 `*.example.com` 通配。
303
+ - 本机模式面向「用户在场」的场景;无人值守/服务端集中模式不在本 SDK 范围。
304
+
305
+ ## 本地开发
306
+
307
+ ```bash
308
+ npm install # 在仓库根目录(workspace)
309
+ npm run build -w sdk # 构建 dist(esbuild 产物 + tsc 类型声明)
310
+ npm run demo -w sdk # 演示页 http://127.0.0.1:4567
311
+ npm run typecheck -w sdk
312
+ ```
313
+
314
+ ## 许可证
315
+
316
+ [MIT](./LICENSE)
@@ -0,0 +1,130 @@
1
+ export type LsagentState = "disconnected" | "connecting" | "connected" | "pending" | "authed";
2
+ export interface LsagentOptions {
3
+ /** 伴侣应用 WS 地址,默认 ws://127.0.0.1:37700/ws(远程模式填 ws://IP:37700/ws) */
4
+ url?: string;
5
+ /** 6 位配对码(用户 `lsagent --show-code` 查看)。推荐不传——免码模式首次弹系统确认,一次点击永久授权 */
6
+ pairingCode?: string;
7
+ }
8
+ /** 公开类型自带(不依赖 @lsagent/shared):SDK 产物零依赖,d.ts 可独立分发 */
9
+ export interface LsagentModel {
10
+ provider: string;
11
+ id: string;
12
+ name: string;
13
+ available: boolean;
14
+ }
15
+ /** 本机模型服务(models.json + auth.json 合并视图;hasKey=false 表示未配密钥不可用) */
16
+ export interface LsagentProvider {
17
+ name: string;
18
+ displayName: string;
19
+ baseUrl: string;
20
+ /** "openai-completions" | "anthropic-messages" | ... */
21
+ apiType: string;
22
+ hasKey: boolean;
23
+ models: {
24
+ id: string;
25
+ name: string;
26
+ contextWindow: number;
27
+ maxTokens: number;
28
+ }[];
29
+ }
30
+ /** providers.save 入参(apiKey 省略/为空 = 保留本机已有 key) */
31
+ export interface LsagentProviderSpec {
32
+ name: string;
33
+ displayName: string;
34
+ baseUrl: string;
35
+ apiType: string;
36
+ apiKey?: string;
37
+ models: {
38
+ id: string;
39
+ name: string;
40
+ reasoning?: boolean;
41
+ contextWindow?: number;
42
+ maxTokens?: number;
43
+ }[];
44
+ }
45
+ export interface LsagentTask {
46
+ id: string;
47
+ title: string;
48
+ status: "idle" | "running" | "error";
49
+ model: string | null;
50
+ cwd: string;
51
+ createdAt: string;
52
+ messageCount: number;
53
+ }
54
+ export interface LsagentMessage {
55
+ role: "user" | "assistant" | "toolResult";
56
+ text: string;
57
+ model?: string;
58
+ errorMessage?: string;
59
+ usage?: {
60
+ input: number;
61
+ output: number;
62
+ cost: number;
63
+ };
64
+ timestamp: number;
65
+ }
66
+ export interface LsagentCreateTaskOptions {
67
+ title?: string;
68
+ /** 模型提供方 + 模型 id(缺省用伴侣应用全局默认模型;也可只建任务后用 prompt 前指定) */
69
+ provider?: string;
70
+ modelId?: string;
71
+ /** 工作目录(本机绝对路径,缺省为伴侣应用 workspace) */
72
+ cwd?: string;
73
+ /** 建任务后立即发送的首条消息 */
74
+ firstMessage?: string;
75
+ /** 追加到系统提示末尾(业务背景、输出格式约定等) */
76
+ systemPromptAppend?: string;
77
+ /** 按名启用的本地 skill */
78
+ skills?: string[];
79
+ /** 仅接入指定 MCP server(缺省接入全部启用的;业务系统自己的工具走这里) */
80
+ mcpServers?: string[];
81
+ }
82
+ export type LsagentEventHandler = (event: string, payload: Record<string, unknown>) => void;
83
+ export type LsagentStateHandler = (state: LsagentState) => void;
84
+ export declare class LsagentClient {
85
+ private readonly inner;
86
+ private stateHandler;
87
+ private eventHandler;
88
+ constructor(opts: LsagentOptions);
89
+ /** 建立连接(自动认证 + 断线指数退避重连;重复调用幂等) */
90
+ connect(): void;
91
+ /** 主动断开(停止重连) */
92
+ disconnect(): void;
93
+ getState(): LsagentState;
94
+ /** 最近一次连接/认证失败原因(诊断用):如"等待用户在本机确认接入…"、"接入被拒绝(denied)" */
95
+ getLastError(): string | null;
96
+ /** 连接状态变化(authed = 配对成功可用;pending = 等用户在本机点系统确认框)。返回取消订阅函数 */
97
+ onState(cb: LsagentStateHandler): () => void;
98
+ /** 伴侣应用事件(全量广播,业务方按 event 名过滤)。返回取消订阅函数
99
+ *
100
+ * 常用事件(均带 taskId):task.status(idle|running|error)、task.delta(流式文本)、
101
+ * task.message(完整消息)、task.tool(工具调用 start|end)、task.usage(用量)、
102
+ * task.deleted;另有 mail.new(新邮件) */
103
+ onEvent(cb: LsagentEventHandler): () => void;
104
+ /** 等待配对完成(connect + 轮询,超时返回 false)——页面初始化或用户点「测试连接」时用。
105
+ * 免码模式下用户点系统确认框可能超过默认 6s:建议先 onState 监听 pending 提示用户,
106
+ * 并把超时放大(如 120_000) */
107
+ waitForAuthed(timeoutMs?: number): Promise<boolean>;
108
+ /** 通用请求(伴侣应用 WS 协议全开放,如 fs.readFile / email.send / flows.list) */
109
+ request<T = unknown>(type: string, payload?: unknown, timeoutMs?: number): Promise<T>;
110
+ listModels(): Promise<LsagentModel[]>;
111
+ /** 本机模型服务列表(探测配置状态:全部 hasKey=false = 用户还没配过 key) */
112
+ listProviders(): Promise<LsagentProvider[]>;
113
+ /** 新增/更新模型服务(写本机 models.json + auth.json,保存即生效;apiKey 为空 = 保留原 key) */
114
+ saveProvider(spec: LsagentProviderSpec): Promise<{
115
+ name: string;
116
+ }>;
117
+ listTasks(): Promise<LsagentTask[]>;
118
+ getMessages(taskId: string): Promise<LsagentMessage[]>;
119
+ /** 建任务(firstMessage 非空则立即开始执行,进度走 task.* 事件) */
120
+ createTask(opts?: LsagentCreateTaskOptions): Promise<LsagentTask>;
121
+ /** 向任务追加消息(任务空闲时触发执行;执行中提交会被拒绝,改用 steer 或等空闲) */
122
+ prompt(taskId: string, text: string): Promise<{
123
+ queued?: boolean;
124
+ }>;
125
+ /** 任务执行中插入转向指令(不排队,下一轮生效) */
126
+ steer(taskId: string, text: string): Promise<unknown>;
127
+ /** 中止执行中的任务 */
128
+ abort(taskId: string): Promise<unknown>;
129
+ }
130
+ export default LsagentClient;
@@ -0,0 +1,328 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // ../shared/src/companion-client.ts
6
+ var CompanionClient = class {
7
+ constructor(handlers, pairingCode, url = "ws://127.0.0.1:37700/ws", client) {
8
+ this.handlers = handlers;
9
+ this.client = client;
10
+ __publicField(this, "ws", null);
11
+ __publicField(this, "pending", /* @__PURE__ */ new Map());
12
+ __publicField(this, "nextId", 1);
13
+ __publicField(this, "reconnectTimer", null);
14
+ __publicField(this, "pendingRetryTimer", null);
15
+ __publicField(this, "backoffMs", 1e3);
16
+ __publicField(this, "stopped", false);
17
+ __publicField(this, "state", "disconnected");
18
+ __publicField(this, "version", null);
19
+ /** 最近一次连接/认证失败的原因(诊断用) */
20
+ __publicField(this, "lastError", null);
21
+ __publicField(this, "pairingCode");
22
+ __publicField(this, "url");
23
+ this.pairingCode = pairingCode;
24
+ this.url = url;
25
+ }
26
+ setState(state) {
27
+ var _a, _b;
28
+ this.state = state;
29
+ (_b = (_a = this.handlers).onStateChange) == null ? void 0 : _b.call(_a, state, this.version, this.lastError);
30
+ }
31
+ connect() {
32
+ this.stopped = false;
33
+ if (this.reconnectTimer !== null) {
34
+ clearTimeout(this.reconnectTimer);
35
+ this.reconnectTimer = null;
36
+ }
37
+ if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
38
+ return;
39
+ }
40
+ this.setState("connecting");
41
+ const ws = new WebSocket(this.url);
42
+ this.ws = ws;
43
+ const isCurrent = () => this.ws === ws;
44
+ ws.onopen = () => {
45
+ if (!isCurrent()) return;
46
+ this.lastError = null;
47
+ this.setState("connected");
48
+ this.authenticate(ws, isCurrent);
49
+ };
50
+ ws.onmessage = (ev) => {
51
+ var _a, _b, _c;
52
+ if (!isCurrent()) return;
53
+ let msg;
54
+ try {
55
+ msg = JSON.parse(String(ev.data));
56
+ } catch (e) {
57
+ return;
58
+ }
59
+ if (msg.type === "event") {
60
+ (_c = (_b = this.handlers).onEvent) == null ? void 0 : _c.call(_b, (_a = msg.event) != null ? _a : "", msg.payload);
61
+ return;
62
+ }
63
+ if (msg.type === "creq") {
64
+ this.handleServerRequest(msg);
65
+ return;
66
+ }
67
+ if (msg.type !== "res") return;
68
+ const entry = this.pending.get(msg.id);
69
+ if (!entry) return;
70
+ this.pending.delete(msg.id);
71
+ clearTimeout(entry.timer);
72
+ if (msg.ok) entry.resolve(msg.data);
73
+ else entry.reject(new Error(msg.error || "\u672A\u77E5\u9519\u8BEF"));
74
+ };
75
+ ws.onclose = (ev) => {
76
+ if (!isCurrent()) return;
77
+ if (this.pendingRetryTimer !== null) {
78
+ clearTimeout(this.pendingRetryTimer);
79
+ this.pendingRetryTimer = null;
80
+ }
81
+ this.version = null;
82
+ this.rejectAll("\u8FDE\u63A5\u5DF2\u65AD\u5F00");
83
+ if (ev.code === 4003 && /denied/i.test(ev.reason)) {
84
+ this.stopped = true;
85
+ this.lastError = `\u63A5\u5165\u88AB\u62D2\u7EDD\uFF08${ev.reason}\uFF09`;
86
+ console.warn("[companion] \u63A5\u5165\u88AB\u62D2\u7EDD:", ev.reason);
87
+ this.setState("disconnected");
88
+ return;
89
+ }
90
+ if (!this.stopped) {
91
+ this.lastError = `\u8FDE\u63A5\u65AD\u5F00 (code=${ev.code}${ev.reason ? `, ${ev.reason}` : ""})\uFF0C${Math.round(this.backoffMs / 1e3)}s \u540E\u91CD\u8BD5`;
92
+ console.warn("[companion] onclose:", ev.code, ev.reason);
93
+ }
94
+ this.setState("disconnected");
95
+ if (!this.stopped) {
96
+ this.reconnectTimer = setTimeout(() => this.connect(), this.backoffMs);
97
+ this.backoffMs = Math.min(this.backoffMs * 2, 15e3);
98
+ }
99
+ };
100
+ ws.onerror = () => {
101
+ if (!isCurrent()) return;
102
+ this.lastError = "WebSocket \u8FDE\u63A5\u5931\u8D25\uFF08\u4F34\u4FA3\u5E94\u7528\u672A\u542F\u52A8\u6216\u7AEF\u53E3\u88AB\u5360\u7528\uFF1F\uFF09";
103
+ console.warn("[companion] onerror");
104
+ };
105
+ }
106
+ /** 发送 auth:配对码为空 = 确认制免码接入(首次由用户在服务端弹的系统确认框放行)。
107
+ * 服务端回 pending-confirm 表示正在等用户点击,3s 后重发 auth 探测结果——
108
+ * 授权落地 → authed;用户点否 → 服务端 4003 denied 断开走 onclose 致停 */
109
+ authenticate(ws, isCurrent) {
110
+ this.request(
111
+ "auth",
112
+ { ...this.pairingCode ? { code: this.pairingCode } : {}, ...this.client ? { client: this.client } : {} },
113
+ 5e3
114
+ ).then((data) => {
115
+ var _a;
116
+ if (!isCurrent() || this.state === "authed") return;
117
+ this.version = (_a = data == null ? void 0 : data.version) != null ? _a : null;
118
+ this.backoffMs = 1e3;
119
+ this.setState("authed");
120
+ }).catch((err) => {
121
+ if (!isCurrent()) return;
122
+ if (err.message === "pending-confirm") {
123
+ this.lastError = "\u7B49\u5F85\u7528\u6237\u5728\u672C\u673A\u786E\u8BA4\u63A5\u5165\u2026";
124
+ this.setState("pending");
125
+ if (this.pendingRetryTimer !== null) clearTimeout(this.pendingRetryTimer);
126
+ this.pendingRetryTimer = setTimeout(() => {
127
+ this.pendingRetryTimer = null;
128
+ if (!isCurrent() || this.stopped || this.state === "authed") return;
129
+ this.authenticate(ws, isCurrent);
130
+ }, 3e3);
131
+ return;
132
+ }
133
+ this.lastError = `\u8BA4\u8BC1\u5931\u8D25: ${err.message}`;
134
+ console.warn("[companion] \u8BA4\u8BC1\u5931\u8D25:", err.message);
135
+ this.setState("connected");
136
+ });
137
+ }
138
+ disconnect() {
139
+ var _a;
140
+ this.stopped = true;
141
+ if (this.reconnectTimer !== null) clearTimeout(this.reconnectTimer);
142
+ if (this.pendingRetryTimer !== null) {
143
+ clearTimeout(this.pendingRetryTimer);
144
+ this.pendingRetryTimer = null;
145
+ }
146
+ (_a = this.ws) == null ? void 0 : _a.close();
147
+ this.ws = null;
148
+ this.rejectAll("\u5DF2\u4E3B\u52A8\u65AD\u5F00");
149
+ this.setState("disconnected");
150
+ }
151
+ setPairingCode(code) {
152
+ this.update(this.url, code);
153
+ }
154
+ /** 更新连接参数(URL/配对码未变则绝不断开现有连接——连接中反复调用只是幂等 no-op;
155
+ * 否则重连。配对码为空 = 免码确认制接入,与有码行为一致(并非断开)。
156
+ * 此前的 disconnect+connect 会让 SW 冷启动时的并发请求产生重连风暴,
157
+ * 旧 socket 迟到的 onclose 把状态打脏) */
158
+ update(url, code) {
159
+ const changed = url !== this.url || code !== this.pairingCode;
160
+ this.url = url;
161
+ this.pairingCode = code;
162
+ if (!changed) {
163
+ this.connect();
164
+ return;
165
+ }
166
+ this.disconnect();
167
+ this.connect();
168
+ }
169
+ /** 等待进入 authed 状态(用于点击测试/保存时确保连接完成)。
170
+ * 进入等待时先立即 connect():不等退避计时——否则一次失败后 backoff 最长 15s,
171
+ * 6s 的等待会在退避期间白白超时(用户看到的正是"伴侣应用未连接") */
172
+ async waitForAuthed(timeoutMs = 6e3) {
173
+ if (this.state !== "authed" && !this.stopped) this.connect();
174
+ const deadline = Date.now() + timeoutMs;
175
+ while (this.state !== "authed" && Date.now() < deadline) {
176
+ await new Promise((r) => setTimeout(r, 200));
177
+ }
178
+ return this.state === "authed";
179
+ }
180
+ request(type, payload, timeoutMs = 1e4) {
181
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
182
+ return Promise.reject(new Error("\u4F34\u4FA3\u5E94\u7528\u672A\u8FDE\u63A5"));
183
+ }
184
+ const id = `r${this.nextId++}`;
185
+ const msg = { id, type, payload };
186
+ return new Promise((resolve, reject) => {
187
+ const timer = setTimeout(() => {
188
+ this.pending.delete(id);
189
+ reject(new Error("\u8BF7\u6C42\u8D85\u65F6"));
190
+ }, timeoutMs);
191
+ this.pending.set(id, { resolve, reject, timer });
192
+ this.ws.send(JSON.stringify(msg));
193
+ });
194
+ }
195
+ /** 反向请求(creq):调 onRequest 处理并回 cres。handler 缺失/抛错都回错误,不让伴侣应用侧挂等超时 */
196
+ handleServerRequest(msg) {
197
+ const send = (ok, data, error) => {
198
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
199
+ try {
200
+ this.ws.send(JSON.stringify({ type: "cres", id: msg.id, ok, ...ok ? { data } : { error } }));
201
+ } catch (e) {
202
+ }
203
+ };
204
+ const handler = this.handlers.onRequest;
205
+ if (!handler) {
206
+ send(false, void 0, "\u8BE5\u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u6B64\u8BF7\u6C42");
207
+ return;
208
+ }
209
+ Promise.resolve().then(() => handler(msg.tool, msg.payload)).then((data) => send(true, data)).catch((err) => send(false, void 0, err instanceof Error ? err.message : String(err)));
210
+ }
211
+ rejectAll(reason) {
212
+ for (const entry of this.pending.values()) {
213
+ clearTimeout(entry.timer);
214
+ entry.reject(new Error(reason));
215
+ }
216
+ this.pending.clear();
217
+ }
218
+ };
219
+
220
+ // src/index.ts
221
+ var LsagentClient = class {
222
+ constructor(opts) {
223
+ this.stateHandler = null;
224
+ this.eventHandler = null;
225
+ var _a, _b;
226
+ this.inner = new CompanionClient(
227
+ {
228
+ onStateChange: (state) => {
229
+ var _a2;
230
+ return (_a2 = this.stateHandler) == null ? void 0 : _a2.call(this, state);
231
+ },
232
+ onEvent: (event, payload) => {
233
+ var _a2;
234
+ return (_a2 = this.eventHandler) == null ? void 0 : _a2.call(this, event, payload != null ? payload : {});
235
+ }
236
+ },
237
+ (_a = opts.pairingCode) != null ? _a : "",
238
+ (_b = opts.url) != null ? _b : "ws://127.0.0.1:37700/ws",
239
+ // 身份 "sdk":不会被伴侣应用登记为浏览器能力宿主(那是插件专用槽位)
240
+ "sdk"
241
+ );
242
+ }
243
+ /** 建立连接(自动认证 + 断线指数退避重连;重复调用幂等) */
244
+ connect() {
245
+ this.inner.connect();
246
+ }
247
+ /** 主动断开(停止重连) */
248
+ disconnect() {
249
+ this.inner.disconnect();
250
+ }
251
+ getState() {
252
+ return this.inner.state;
253
+ }
254
+ /** 最近一次连接/认证失败原因(诊断用):如"等待用户在本机确认接入…"、"接入被拒绝(denied)" */
255
+ getLastError() {
256
+ return this.inner.lastError;
257
+ }
258
+ /** 连接状态变化(authed = 配对成功可用;pending = 等用户在本机点系统确认框)。返回取消订阅函数 */
259
+ onState(cb) {
260
+ this.stateHandler = cb;
261
+ return () => {
262
+ if (this.stateHandler === cb) this.stateHandler = null;
263
+ };
264
+ }
265
+ /** 伴侣应用事件(全量广播,业务方按 event 名过滤)。返回取消订阅函数
266
+ *
267
+ * 常用事件(均带 taskId):task.status(idle|running|error)、task.delta(流式文本)、
268
+ * task.message(完整消息)、task.tool(工具调用 start|end)、task.usage(用量)、
269
+ * task.deleted;另有 mail.new(新邮件) */
270
+ onEvent(cb) {
271
+ this.eventHandler = cb;
272
+ return () => {
273
+ if (this.eventHandler === cb) this.eventHandler = null;
274
+ };
275
+ }
276
+ /** 等待配对完成(connect + 轮询,超时返回 false)——页面初始化或用户点「测试连接」时用。
277
+ * 免码模式下用户点系统确认框可能超过默认 6s:建议先 onState 监听 pending 提示用户,
278
+ * 并把超时放大(如 120_000) */
279
+ async waitForAuthed(timeoutMs = 6e3) {
280
+ return this.inner.waitForAuthed(timeoutMs);
281
+ }
282
+ /** 通用请求(伴侣应用 WS 协议全开放,如 fs.readFile / email.send / flows.list) */
283
+ request(type, payload, timeoutMs) {
284
+ return this.inner.request(type, payload, timeoutMs);
285
+ }
286
+ async listModels() {
287
+ const data = await this.request("models.list");
288
+ return data.models;
289
+ }
290
+ /** 本机模型服务列表(探测配置状态:全部 hasKey=false = 用户还没配过 key) */
291
+ async listProviders() {
292
+ const data = await this.request("providers.list");
293
+ return data.providers;
294
+ }
295
+ /** 新增/更新模型服务(写本机 models.json + auth.json,保存即生效;apiKey 为空 = 保留原 key) */
296
+ async saveProvider(spec) {
297
+ return this.request("providers.save", { provider: spec });
298
+ }
299
+ async listTasks() {
300
+ const data = await this.request("task.list");
301
+ return data.tasks;
302
+ }
303
+ async getMessages(taskId) {
304
+ const data = await this.request("task.messages", { taskId });
305
+ return data.messages;
306
+ }
307
+ /** 建任务(firstMessage 非空则立即开始执行,进度走 task.* 事件) */
308
+ async createTask(opts = {}) {
309
+ return this.request("task.create", opts);
310
+ }
311
+ /** 向任务追加消息(任务空闲时触发执行;执行中提交会被拒绝,改用 steer 或等空闲) */
312
+ async prompt(taskId, text) {
313
+ return this.request("task.prompt", { taskId, text });
314
+ }
315
+ /** 任务执行中插入转向指令(不排队,下一轮生效) */
316
+ async steer(taskId, text) {
317
+ return this.request("task.steer", { taskId, text });
318
+ }
319
+ /** 中止执行中的任务 */
320
+ async abort(taskId) {
321
+ return this.request("task.abort", { taskId });
322
+ }
323
+ };
324
+ var index_default = LsagentClient;
325
+ export {
326
+ LsagentClient,
327
+ index_default as default
328
+ };
@@ -0,0 +1,351 @@
1
+ "use strict";
2
+ var LsagentSDK = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
22
+
23
+ // src/index.ts
24
+ var index_exports = {};
25
+ __export(index_exports, {
26
+ LsagentClient: () => LsagentClient,
27
+ default: () => index_default
28
+ });
29
+
30
+ // ../shared/src/companion-client.ts
31
+ var CompanionClient = class {
32
+ constructor(handlers, pairingCode, url = "ws://127.0.0.1:37700/ws", client) {
33
+ this.handlers = handlers;
34
+ this.client = client;
35
+ __publicField(this, "ws", null);
36
+ __publicField(this, "pending", /* @__PURE__ */ new Map());
37
+ __publicField(this, "nextId", 1);
38
+ __publicField(this, "reconnectTimer", null);
39
+ __publicField(this, "pendingRetryTimer", null);
40
+ __publicField(this, "backoffMs", 1e3);
41
+ __publicField(this, "stopped", false);
42
+ __publicField(this, "state", "disconnected");
43
+ __publicField(this, "version", null);
44
+ /** 最近一次连接/认证失败的原因(诊断用) */
45
+ __publicField(this, "lastError", null);
46
+ __publicField(this, "pairingCode");
47
+ __publicField(this, "url");
48
+ this.pairingCode = pairingCode;
49
+ this.url = url;
50
+ }
51
+ setState(state) {
52
+ var _a, _b;
53
+ this.state = state;
54
+ (_b = (_a = this.handlers).onStateChange) == null ? void 0 : _b.call(_a, state, this.version, this.lastError);
55
+ }
56
+ connect() {
57
+ this.stopped = false;
58
+ if (this.reconnectTimer !== null) {
59
+ clearTimeout(this.reconnectTimer);
60
+ this.reconnectTimer = null;
61
+ }
62
+ if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
63
+ return;
64
+ }
65
+ this.setState("connecting");
66
+ const ws = new WebSocket(this.url);
67
+ this.ws = ws;
68
+ const isCurrent = () => this.ws === ws;
69
+ ws.onopen = () => {
70
+ if (!isCurrent()) return;
71
+ this.lastError = null;
72
+ this.setState("connected");
73
+ this.authenticate(ws, isCurrent);
74
+ };
75
+ ws.onmessage = (ev) => {
76
+ var _a, _b, _c;
77
+ if (!isCurrent()) return;
78
+ let msg;
79
+ try {
80
+ msg = JSON.parse(String(ev.data));
81
+ } catch (e) {
82
+ return;
83
+ }
84
+ if (msg.type === "event") {
85
+ (_c = (_b = this.handlers).onEvent) == null ? void 0 : _c.call(_b, (_a = msg.event) != null ? _a : "", msg.payload);
86
+ return;
87
+ }
88
+ if (msg.type === "creq") {
89
+ this.handleServerRequest(msg);
90
+ return;
91
+ }
92
+ if (msg.type !== "res") return;
93
+ const entry = this.pending.get(msg.id);
94
+ if (!entry) return;
95
+ this.pending.delete(msg.id);
96
+ clearTimeout(entry.timer);
97
+ if (msg.ok) entry.resolve(msg.data);
98
+ else entry.reject(new Error(msg.error || "\u672A\u77E5\u9519\u8BEF"));
99
+ };
100
+ ws.onclose = (ev) => {
101
+ if (!isCurrent()) return;
102
+ if (this.pendingRetryTimer !== null) {
103
+ clearTimeout(this.pendingRetryTimer);
104
+ this.pendingRetryTimer = null;
105
+ }
106
+ this.version = null;
107
+ this.rejectAll("\u8FDE\u63A5\u5DF2\u65AD\u5F00");
108
+ if (ev.code === 4003 && /denied/i.test(ev.reason)) {
109
+ this.stopped = true;
110
+ this.lastError = `\u63A5\u5165\u88AB\u62D2\u7EDD\uFF08${ev.reason}\uFF09`;
111
+ console.warn("[companion] \u63A5\u5165\u88AB\u62D2\u7EDD:", ev.reason);
112
+ this.setState("disconnected");
113
+ return;
114
+ }
115
+ if (!this.stopped) {
116
+ this.lastError = `\u8FDE\u63A5\u65AD\u5F00 (code=${ev.code}${ev.reason ? `, ${ev.reason}` : ""})\uFF0C${Math.round(this.backoffMs / 1e3)}s \u540E\u91CD\u8BD5`;
117
+ console.warn("[companion] onclose:", ev.code, ev.reason);
118
+ }
119
+ this.setState("disconnected");
120
+ if (!this.stopped) {
121
+ this.reconnectTimer = setTimeout(() => this.connect(), this.backoffMs);
122
+ this.backoffMs = Math.min(this.backoffMs * 2, 15e3);
123
+ }
124
+ };
125
+ ws.onerror = () => {
126
+ if (!isCurrent()) return;
127
+ this.lastError = "WebSocket \u8FDE\u63A5\u5931\u8D25\uFF08\u4F34\u4FA3\u5E94\u7528\u672A\u542F\u52A8\u6216\u7AEF\u53E3\u88AB\u5360\u7528\uFF1F\uFF09";
128
+ console.warn("[companion] onerror");
129
+ };
130
+ }
131
+ /** 发送 auth:配对码为空 = 确认制免码接入(首次由用户在服务端弹的系统确认框放行)。
132
+ * 服务端回 pending-confirm 表示正在等用户点击,3s 后重发 auth 探测结果——
133
+ * 授权落地 → authed;用户点否 → 服务端 4003 denied 断开走 onclose 致停 */
134
+ authenticate(ws, isCurrent) {
135
+ this.request(
136
+ "auth",
137
+ { ...this.pairingCode ? { code: this.pairingCode } : {}, ...this.client ? { client: this.client } : {} },
138
+ 5e3
139
+ ).then((data) => {
140
+ var _a;
141
+ if (!isCurrent() || this.state === "authed") return;
142
+ this.version = (_a = data == null ? void 0 : data.version) != null ? _a : null;
143
+ this.backoffMs = 1e3;
144
+ this.setState("authed");
145
+ }).catch((err) => {
146
+ if (!isCurrent()) return;
147
+ if (err.message === "pending-confirm") {
148
+ this.lastError = "\u7B49\u5F85\u7528\u6237\u5728\u672C\u673A\u786E\u8BA4\u63A5\u5165\u2026";
149
+ this.setState("pending");
150
+ if (this.pendingRetryTimer !== null) clearTimeout(this.pendingRetryTimer);
151
+ this.pendingRetryTimer = setTimeout(() => {
152
+ this.pendingRetryTimer = null;
153
+ if (!isCurrent() || this.stopped || this.state === "authed") return;
154
+ this.authenticate(ws, isCurrent);
155
+ }, 3e3);
156
+ return;
157
+ }
158
+ this.lastError = `\u8BA4\u8BC1\u5931\u8D25: ${err.message}`;
159
+ console.warn("[companion] \u8BA4\u8BC1\u5931\u8D25:", err.message);
160
+ this.setState("connected");
161
+ });
162
+ }
163
+ disconnect() {
164
+ var _a;
165
+ this.stopped = true;
166
+ if (this.reconnectTimer !== null) clearTimeout(this.reconnectTimer);
167
+ if (this.pendingRetryTimer !== null) {
168
+ clearTimeout(this.pendingRetryTimer);
169
+ this.pendingRetryTimer = null;
170
+ }
171
+ (_a = this.ws) == null ? void 0 : _a.close();
172
+ this.ws = null;
173
+ this.rejectAll("\u5DF2\u4E3B\u52A8\u65AD\u5F00");
174
+ this.setState("disconnected");
175
+ }
176
+ setPairingCode(code) {
177
+ this.update(this.url, code);
178
+ }
179
+ /** 更新连接参数(URL/配对码未变则绝不断开现有连接——连接中反复调用只是幂等 no-op;
180
+ * 否则重连。配对码为空 = 免码确认制接入,与有码行为一致(并非断开)。
181
+ * 此前的 disconnect+connect 会让 SW 冷启动时的并发请求产生重连风暴,
182
+ * 旧 socket 迟到的 onclose 把状态打脏) */
183
+ update(url, code) {
184
+ const changed = url !== this.url || code !== this.pairingCode;
185
+ this.url = url;
186
+ this.pairingCode = code;
187
+ if (!changed) {
188
+ this.connect();
189
+ return;
190
+ }
191
+ this.disconnect();
192
+ this.connect();
193
+ }
194
+ /** 等待进入 authed 状态(用于点击测试/保存时确保连接完成)。
195
+ * 进入等待时先立即 connect():不等退避计时——否则一次失败后 backoff 最长 15s,
196
+ * 6s 的等待会在退避期间白白超时(用户看到的正是"伴侣应用未连接") */
197
+ async waitForAuthed(timeoutMs = 6e3) {
198
+ if (this.state !== "authed" && !this.stopped) this.connect();
199
+ const deadline = Date.now() + timeoutMs;
200
+ while (this.state !== "authed" && Date.now() < deadline) {
201
+ await new Promise((r) => setTimeout(r, 200));
202
+ }
203
+ return this.state === "authed";
204
+ }
205
+ request(type, payload, timeoutMs = 1e4) {
206
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
207
+ return Promise.reject(new Error("\u4F34\u4FA3\u5E94\u7528\u672A\u8FDE\u63A5"));
208
+ }
209
+ const id = `r${this.nextId++}`;
210
+ const msg = { id, type, payload };
211
+ return new Promise((resolve, reject) => {
212
+ const timer = setTimeout(() => {
213
+ this.pending.delete(id);
214
+ reject(new Error("\u8BF7\u6C42\u8D85\u65F6"));
215
+ }, timeoutMs);
216
+ this.pending.set(id, { resolve, reject, timer });
217
+ this.ws.send(JSON.stringify(msg));
218
+ });
219
+ }
220
+ /** 反向请求(creq):调 onRequest 处理并回 cres。handler 缺失/抛错都回错误,不让伴侣应用侧挂等超时 */
221
+ handleServerRequest(msg) {
222
+ const send = (ok, data, error) => {
223
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
224
+ try {
225
+ this.ws.send(JSON.stringify({ type: "cres", id: msg.id, ok, ...ok ? { data } : { error } }));
226
+ } catch (e) {
227
+ }
228
+ };
229
+ const handler = this.handlers.onRequest;
230
+ if (!handler) {
231
+ send(false, void 0, "\u8BE5\u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u6B64\u8BF7\u6C42");
232
+ return;
233
+ }
234
+ Promise.resolve().then(() => handler(msg.tool, msg.payload)).then((data) => send(true, data)).catch((err) => send(false, void 0, err instanceof Error ? err.message : String(err)));
235
+ }
236
+ rejectAll(reason) {
237
+ for (const entry of this.pending.values()) {
238
+ clearTimeout(entry.timer);
239
+ entry.reject(new Error(reason));
240
+ }
241
+ this.pending.clear();
242
+ }
243
+ };
244
+
245
+ // src/index.ts
246
+ var LsagentClient = class {
247
+ constructor(opts) {
248
+ this.stateHandler = null;
249
+ this.eventHandler = null;
250
+ var _a, _b;
251
+ this.inner = new CompanionClient(
252
+ {
253
+ onStateChange: (state) => {
254
+ var _a2;
255
+ return (_a2 = this.stateHandler) == null ? void 0 : _a2.call(this, state);
256
+ },
257
+ onEvent: (event, payload) => {
258
+ var _a2;
259
+ return (_a2 = this.eventHandler) == null ? void 0 : _a2.call(this, event, payload != null ? payload : {});
260
+ }
261
+ },
262
+ (_a = opts.pairingCode) != null ? _a : "",
263
+ (_b = opts.url) != null ? _b : "ws://127.0.0.1:37700/ws",
264
+ // 身份 "sdk":不会被伴侣应用登记为浏览器能力宿主(那是插件专用槽位)
265
+ "sdk"
266
+ );
267
+ }
268
+ /** 建立连接(自动认证 + 断线指数退避重连;重复调用幂等) */
269
+ connect() {
270
+ this.inner.connect();
271
+ }
272
+ /** 主动断开(停止重连) */
273
+ disconnect() {
274
+ this.inner.disconnect();
275
+ }
276
+ getState() {
277
+ return this.inner.state;
278
+ }
279
+ /** 最近一次连接/认证失败原因(诊断用):如"等待用户在本机确认接入…"、"接入被拒绝(denied)" */
280
+ getLastError() {
281
+ return this.inner.lastError;
282
+ }
283
+ /** 连接状态变化(authed = 配对成功可用;pending = 等用户在本机点系统确认框)。返回取消订阅函数 */
284
+ onState(cb) {
285
+ this.stateHandler = cb;
286
+ return () => {
287
+ if (this.stateHandler === cb) this.stateHandler = null;
288
+ };
289
+ }
290
+ /** 伴侣应用事件(全量广播,业务方按 event 名过滤)。返回取消订阅函数
291
+ *
292
+ * 常用事件(均带 taskId):task.status(idle|running|error)、task.delta(流式文本)、
293
+ * task.message(完整消息)、task.tool(工具调用 start|end)、task.usage(用量)、
294
+ * task.deleted;另有 mail.new(新邮件) */
295
+ onEvent(cb) {
296
+ this.eventHandler = cb;
297
+ return () => {
298
+ if (this.eventHandler === cb) this.eventHandler = null;
299
+ };
300
+ }
301
+ /** 等待配对完成(connect + 轮询,超时返回 false)——页面初始化或用户点「测试连接」时用。
302
+ * 免码模式下用户点系统确认框可能超过默认 6s:建议先 onState 监听 pending 提示用户,
303
+ * 并把超时放大(如 120_000) */
304
+ async waitForAuthed(timeoutMs = 6e3) {
305
+ return this.inner.waitForAuthed(timeoutMs);
306
+ }
307
+ /** 通用请求(伴侣应用 WS 协议全开放,如 fs.readFile / email.send / flows.list) */
308
+ request(type, payload, timeoutMs) {
309
+ return this.inner.request(type, payload, timeoutMs);
310
+ }
311
+ async listModels() {
312
+ const data = await this.request("models.list");
313
+ return data.models;
314
+ }
315
+ /** 本机模型服务列表(探测配置状态:全部 hasKey=false = 用户还没配过 key) */
316
+ async listProviders() {
317
+ const data = await this.request("providers.list");
318
+ return data.providers;
319
+ }
320
+ /** 新增/更新模型服务(写本机 models.json + auth.json,保存即生效;apiKey 为空 = 保留原 key) */
321
+ async saveProvider(spec) {
322
+ return this.request("providers.save", { provider: spec });
323
+ }
324
+ async listTasks() {
325
+ const data = await this.request("task.list");
326
+ return data.tasks;
327
+ }
328
+ async getMessages(taskId) {
329
+ const data = await this.request("task.messages", { taskId });
330
+ return data.messages;
331
+ }
332
+ /** 建任务(firstMessage 非空则立即开始执行,进度走 task.* 事件) */
333
+ async createTask(opts = {}) {
334
+ return this.request("task.create", opts);
335
+ }
336
+ /** 向任务追加消息(任务空闲时触发执行;执行中提交会被拒绝,改用 steer 或等空闲) */
337
+ async prompt(taskId, text) {
338
+ return this.request("task.prompt", { taskId, text });
339
+ }
340
+ /** 任务执行中插入转向指令(不排队,下一轮生效) */
341
+ async steer(taskId, text) {
342
+ return this.request("task.steer", { taskId, text });
343
+ }
344
+ /** 中止执行中的任务 */
345
+ async abort(taskId) {
346
+ return this.request("task.abort", { taskId });
347
+ }
348
+ };
349
+ var index_default = LsagentClient;
350
+ return __toCommonJS(index_exports);
351
+ })();
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "lsagent-sdk",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "description": "浏览器 JS SDK——网页连接用户本机的 lsagent AI 运行时(ws://127.0.0.1:37700/ws):建任务、流式事件、文件上传、模型配置引导",
6
+ "license": "MIT",
7
+ "main": "./dist/lsagent-sdk.esm.js",
8
+ "module": "./dist/lsagent-sdk.esm.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/lsagent-sdk.esm.js",
14
+ "default": "./dist/lsagent-sdk.js"
15
+ },
16
+ "./dist/*": "./dist/*"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "build": "node scripts/build.mjs",
25
+ "typecheck": "tsc --noEmit",
26
+ "demo": "node demo/server.mjs"
27
+ },
28
+ "devDependencies": {
29
+ "@lsagent/shared": "*",
30
+ "esbuild": "^0.25.0",
31
+ "typescript": "^5.7.2"
32
+ }
33
+ }