@blade-hq/agent-client 2608.0.2 → 2608.0.3

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 CHANGED
@@ -76,6 +76,15 @@ await client.auth.getProviders() // 服务端支持的登录方式
76
76
 
77
77
  `login()` 失败时的错误都带中文原因:弹窗被拦截、窗口被关闭、超时。
78
78
 
79
+ 令牌要留在自己的服务端时,浏览器只把授权回调拿到的一次性 code 交给后端,由后端换:
80
+
81
+ ```ts
82
+ // ExchangeCodeParams -> ExchangeCodeResult
83
+ const { access_token } = await client.auth.exchangeCode({ code, state, clientOrigin })
84
+ ```
85
+
86
+ `clientOrigin` 要和发起授权时用的 origin 一致;服务端调用会自动补 `Origin` 头。
87
+
79
88
  ## 会话:client.sessions
80
89
 
81
90
  ### 实时对话(推荐入口)
@@ -130,6 +139,73 @@ await client.sessions.deleteSession(id)
130
139
  await client.sessions.getSessionTurns(id) // 历史消息(投影格式,与实时流同构)
131
140
  ```
132
141
 
142
+ ### 会话回放(演示 / 彩排)
143
+
144
+ 拿一个已经聊完的会话当素材,重现当时的回复和工具调用,**完全不调用模型**。
145
+ 用来做演示和彩排:离线可跑、不花钱、不会临场翻车。
146
+
147
+ **它不是把现有会话切成回放模式,而是派生一个新会话**,源会话原样不动:
148
+
149
+ ```ts
150
+ // 1. 能不能拿它当素材(有并行子智能体、运行快照不完整的不行)
151
+ const { supported, reason } = await client.sessions.getReplayPreview(sourceId)
152
+ if (!supported) return show(reason) // reason 可直接展示给用户
153
+
154
+ // 2. 派生回放会话——这一步才是"进入回放",返回的是一个全新的 session_id
155
+ const { session_id: replayId } = await client.sessions.startReplaySession(sourceId)
156
+ const fast = await client.sessions.startReplaySession(sourceId, 2) // 或显式 1 | 2 | 5
157
+
158
+ // 3. 连上它,一出生就带着 replay 状态
159
+ const session = await client.sessions.connect(replayId)
160
+ session.getState().replay
161
+ // → { isReplay: true, speed: 5, sourceSessionId: sourceId }
162
+ ```
163
+
164
+ 不传倍速时用 `DEFAULT_REPLAY_SPEED`;所有创建入口都该用它,免得同一个源对话
165
+ 从不同入口开出来速度不一样。
166
+
167
+ **连上之后不会自动播——它跟用户对台词**(下面的 `session` 都是第 3 步连上的**回放会话**):
168
+
169
+ ```ts
170
+ // 源会话当初第一句问的是"帮我查下当前目录"
171
+ await session.send("帮我查下当前目录") // 对得上:按倍速重现当时的回复和工具调用
172
+ await session.send("今天天气怎么样") // 对不上:抛 replayMismatch 等你决定(见下)
173
+ ```
174
+
175
+ **回放中的状态和动作**都挂在会话上,和 `send()` 同级:
176
+
177
+ ```ts
178
+ session.getState().replay // { isReplay, speed, sourceSessionId } | null(不是回放会话)
179
+ session.getState().viewerRole // "viewer" 表示只读,改不动回放
180
+ await session.setReplaySpeed(2)
181
+ await session.exitReplay() // 退出回放,之后的对话真的运行
182
+ ```
183
+
184
+ 改回放是 owner 专属(服务端 `PATCH` 走 owner 校验),只读身份下这两个动作是 no-op。
185
+
186
+ `replay` 是 `SessionState` 的一部分,跟着快照订阅走——Vue 直接接 `shallowRef` 即可,
187
+ 不用自己拉状态:
188
+
189
+ ```ts
190
+ const state = shallowRef(session.getState()) // session = connect(replayId) 的返回值
191
+ session.subscribe(() => (state.value = session.getState()))
192
+ // 模板里 state.value.replay?.isReplay / state.value.replay?.speed
193
+ ```
194
+
195
+ 连接会话时会一并拉好初始回放状态,所以第一帧就知道自己在不在回放。
196
+
197
+ **输入冲突**:用户说的话和录制内容对不上时,服务端会推 `replayMismatch`,必须在事件里给出决定:
198
+
199
+ ```ts
200
+ session.on("replayMismatch", ({ expectedMessage, actualMessage, respond }) => {
201
+ respond("keep_replay") // 按录制内容继续
202
+ // respond("continue_replay") // 从这里开始真的运行
203
+ })
204
+ ```
205
+
206
+ `replayMismatch` **只交给最先注册的处理器**;一个都没注册时默认 `continue_replay`
207
+ (静默转为真实运行)。React 应用可直接用 `@blade-hq/agent-react` 的 `useReplay()`。
208
+
133
209
  ### 工作区文件
134
210
 
135
211
  ```ts
@@ -151,8 +227,10 @@ await chat.downloadFile("输出/简历.md", "简历.md") // 浏览器下载
151
227
  ### 模型目录
152
228
 
153
229
  ```ts
154
- const { default: defaultModel, models } = await client.models.list()
230
+ const { default: defaultModel, models, baseUrl } = await client.models.list()
155
231
  // models: Array<{ id, label }>
232
+ // baseUrl: 平台默认模型服务的 OpenAI 兼容地址,自建应用带用户令牌可直接调;
233
+ // 各部署端口不同,别写死,老版本 Server 返回空串。
156
234
  ```
157
235
 
158
236
  ## AgentSession
@@ -171,6 +249,8 @@ chat.getState()
171
249
  // mode, "planning" | "executing" | null
172
250
  // connection, 连接状态:"connected" | "connecting" | "reconnecting" | "disconnected"
173
251
  // errorMessage, 最近一次运行错误
252
+ // replay, 回放状态:{ isReplay, speed, sourceSessionId } | null
253
+ // viewerRole, "owner" | "viewer" | null(只读身份改不动会话)
174
254
  // turns, askAnswers, agentLoops, activeCompaction 进阶字段
175
255
  // }
176
256
 
@@ -365,6 +445,29 @@ function Message({ message }: { message: ChatMessage }) {
365
445
  }
366
446
  ```
367
447
 
448
+ ### 让用户挑一个技能来做事
449
+
450
+ 自己实现技能选择器时,用 `transformSlashCommand` 把选中的技能翻译成智能体能执行的一段话:
451
+
452
+ ```ts
453
+ import { transformSlashCommand, type SkillMentionAvailability } from "@blade-hq/agent-client"
454
+
455
+ transformSlashCommand("org/data-analysis", "分析这份季度报表")
456
+ // "请使用 org/data-analysis skill 完成任务\n分析这份季度报表"
457
+ ```
458
+
459
+ 第三个参数说明这个技能眼下能不能直接用。技能装没装、加载没加载,用户不需要知道,但智能体需要——传进来之后它会被翻译成对应的 CLI 步骤:
460
+
461
+ ```ts
462
+ // Blade Hub 上已安装、但当前会话还没加载:要求先 use
463
+ transformSlashCommand(skillId, prompt, { local: false, installed: true })
464
+
465
+ // Blade Hub 上还没安装:要求先 install 再 use
466
+ transformSlashCommand(skillId, prompt, { local: false, installed: false })
467
+ ```
468
+
469
+ 不传第三个参数时按本地已有处理,和不带这个参数的老用法结果一致。
470
+
368
471
  ## 常见问题
369
472
 
370
473
  | 现象 | 原因与解法 |
@@ -386,9 +489,10 @@ function Message({ message }: { message: ChatMessage }) {
386
489
  - **声明式会话**:`SessionDefinition`、`SolutionDefinition`、`SkillDefinition`、`SessionConfig`、`TextFile`、`SessionSetupError`、`SessionSetupStage`
387
490
  - **模型目录**:`ModelsResource`、`ModelCatalog`、`ModelOption`
388
491
  - **会话资源(REST)**:`SessionsResource`、`CreateSessionRequest`、`ImportSessionOptions`、`AppCliDefinition`、`AppCliAttachment`、`AttachAppOptions`、`PaginatedSessionsResult`、`SessionHistory`、`SessionContextStats`、`ShareLinkResult`、`FileEntry`、`UploadFileEntry`、`UploadFilesOptions`、`SessionProfile`、`SessionDetail`、`SessionInfo`、`SessionStatus`、`SessionPortMapping`、`ModeId`、`TemplateId`、`PrimarySkillSnapshot`、`PrimarySkillParallelMode`
492
+ - **会话回放**:`ReplayState`、`ReplaySpeed`、`ReplayPreview`、`ReplaySnapshot`、`toReplaySnapshot`、`DEFAULT_REPLAY_SPEED`
389
493
  - **会话状态机**:`SessionHub`、`SessionState`、`SendOptions`、`ConnectionStatus`、`AskUserAnswerData`、`AgentLoopInfo`、`ActiveCompactionState`、`createInitialSessionState`、`AgentSessionEventName`
390
494
  - **页面协作**:`EmbeddedChat`、`EmbeddedChatOptions`、`CommandHandler`、`CommandEnvelope`、`InboundAction`、`InboundEnvelope`、`isCommandEnvelope`、`isInboundEnvelope`
391
- - **消息与投影协议**:`MessageContent`、`MessageContentPart`、`TextContentPart`、`ImageUrlContentPart`、`FileContentPart`、`ToolCallInfo`、`ToolBridgeContent`、`CompactionInfo`、`MemoryRefInfo`、`ArchivedFileInfo`、`ArchivedToolCallInfo`、`TurnProjection`、`ContentBlock`、`PatchEnvelope`、`MemoryRef`、`buildMessageContent`、`normalizeMessageContent`、`isHiddenInternalMessage`、`transformSlashCommand`、`extractTextAttachments`、`ParsedTextAttachment`、`ParsedTextContext`
495
+ - **消息与投影协议**:`MessageContent`、`MessageContentPart`、`TextContentPart`、`ImageUrlContentPart`、`FileContentPart`、`ToolCallInfo`、`ToolBridgeContent`、`CompactionInfo`、`MemoryRefInfo`、`ArchivedFileInfo`、`ArchivedToolCallInfo`、`TurnProjection`、`ContentBlock`、`PatchEnvelope`、`MemoryRef`、`buildMessageContent`、`normalizeMessageContent`、`isHiddenInternalMessage`、`transformSlashCommand`、`SkillMentionAvailability`、`extractTextAttachments`、`ParsedTextAttachment`、`ParsedTextContext`
392
496
  - **Solution / 任务协议**:`Solution`、`SolutionAppField`、`SolutionAppState`、`SolutionAppUiConfig`、`SolutionRef`、`PublishedSolutionRef`、`ExistingSolutionRef`、`PreparedSolution`、`PreparedSolutionAsset`、`LayoutType`、`BizRole`、`TaskStatus`、`BackgroundTask`、`BackgroundTaskStopResult`
393
497
  - **Headless**:`HeadlessResource`、`RunOptions`、`RunResult`、`RunTrace`
394
498
  - **低层通道(apps/web 等高级集成)**:`createSocket`、`CreateSocketOptions`、`TypedSocket`、`AsrAudioPayload`、`ClientProjectionBuilder`、`RawEvent`
package/dist/index.d.ts CHANGED
@@ -9,14 +9,14 @@ export { SessionHub } from "./session/hub";
9
9
  export type { AgentSessionEvents, AgentSessionEventName } from "./session/events";
10
10
  export { SessionSetupError } from "./session/definition";
11
11
  export type { SessionConfig, SessionDefinition, SessionSetupStage, SkillDefinition, TextFile, SolutionDefinition, } from "./session/definition";
12
- export { createInitialSessionState } from "./session/state";
13
- export type { AskUserAnswerData, AgentLoopInfo, ActiveCompactionState, ConnectionStatus, SessionState, } from "./session/state";
12
+ export { createInitialSessionState, toReplaySnapshot } from "./session/state";
13
+ export type { AskUserAnswerData, AgentLoopInfo, ActiveCompactionState, ConnectionStatus, ReplaySnapshot, SessionState, } from "./session/state";
14
14
  export { connectEmbedded } from "./commands/embedded";
15
15
  export type { EmbeddedChat, EmbeddedChatOptions } from "./commands/embedded";
16
16
  export type { CommandHandler } from "./commands/registry";
17
17
  export type { CommandEnvelope, InboundAction, InboundEnvelope } from "./commands/protocol";
18
18
  export { isCommandEnvelope, isInboundEnvelope } from "./commands/protocol";
19
- export type { AuthResource, ProvidersResponse, UserInfo } from "./resources/auth";
19
+ export type { AuthResource, ExchangeCodeParams, ExchangeCodeResult, ProvidersResponse, UserInfo, } from "./resources/auth";
20
20
  export type { HeadlessResource } from "./resources/headless";
21
21
  export { ModelsResource } from "./resources/models";
22
22
  export type { ModelCatalog, ModelOption } from "./resources/models";
@@ -24,10 +24,11 @@ export type { SessionsResource } from "./resources/sessions";
24
24
  export type { CreateSessionRequest, AppCliAttachment, AppCliDefinition, FileEntry, ImportSessionOptions, PaginatedSessionsResult, SessionContextStats, SessionHistory, ShareLinkResult, UploadFileEntry, UploadFilesOptions, } from "./resources/sessions";
25
25
  export type { ArchivedFileInfo, ArchivedToolCallInfo, ChatMessage, CompactionInfo, FileContentPart, ImageUrlContentPart, MemoryRefInfo, MessageContent, MessageContentPart, TextContentPart, ToolBridgeContent, ToolCallInfo, } from "./schemas/message";
26
26
  export { buildMessageContent, contentPreview, extractTextAttachments, getFileParts, getImageParts, getTextContent, groupMessagesByLoop, isHiddenInternalMessage, normalizeMessageContent, transformSlashCommand, } from "./schemas/message-utils";
27
- export type { ParsedTextAttachment, ParsedTextContext } from "./schemas/message-utils";
27
+ export type { ParsedTextAttachment, ParsedTextContext, SkillMentionAvailability, } from "./schemas/message-utils";
28
28
  export type { ContentBlock, MemoryRef, PatchEnvelope, TurnProjection } from "./schemas/projection";
29
+ export { DEFAULT_REPLAY_SPEED } from "./schemas/session";
29
30
  export { SessionInfo, SessionStatus } from "./schemas/session";
30
- export type { ModeId, PrimarySkillParallelMode, PrimarySkillSnapshot, SessionDetail, SessionPortMapping, PublishedSolutionRef, TemplateId, } from "./schemas/session";
31
+ export type { ModeId, PrimarySkillParallelMode, PrimarySkillSnapshot, ReplayPreview, ReplaySpeed, ReplayState, SessionDetail, SessionPortMapping, PublishedSolutionRef, TemplateId, } from "./schemas/session";
31
32
  export { LayoutType } from "./schemas/solution";
32
33
  export type { BizRole, Solution, SolutionAppField, SolutionAppState, SolutionAppUiConfig, } from "./schemas/solution";
33
34
  export { Task, TaskStatus } from "./schemas/task";
package/dist/index.js CHANGED
@@ -2,6 +2,9 @@
2
2
  function isAuthCallbackMessage(value) {
3
3
  return typeof value === "object" && value !== null && value.type === "blade-agent:sdk-auth";
4
4
  }
5
+ var AUTH_ERROR_HINTS = {
6
+ pat_unavailable: "\u6CA1\u80FD\u4E3A\u4F60\u51C6\u5907\u8BBF\u95EE\u51ED\u636E\u3002\u8BF7\u9000\u51FA\u540E\u91CD\u65B0\u767B\u5F55 Blade \u518D\u8BD5\u4E00\u6B21\uFF1B\u82E5\u4ECD\u7136\u5931\u8D25\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u3002"
7
+ };
5
8
  function randomState() {
6
9
  const bytes = new Uint8Array(24);
7
10
  crypto.getRandomValues(bytes);
@@ -79,18 +82,22 @@ async function loginWithPopup(client, options = {}) {
79
82
  if (event.data.state !== state) return;
80
83
  const message = event.data;
81
84
  stopCloseWatch();
82
- try {
83
- popup.close();
84
- } catch {
85
- }
85
+ const closePopup = () => {
86
+ try {
87
+ popup.close();
88
+ } catch {
89
+ }
90
+ };
86
91
  if (message.status === "denied") {
92
+ closePopup();
87
93
  settle(() => reject(new Error("\u4F60\u53D6\u6D88\u4E86\u6388\u6743\uFF0C\u672A\u5B8C\u6210\u767B\u5F55\u3002")));
88
94
  return;
89
95
  }
90
96
  if (message.status !== "success" || !message.code) {
91
- settle(() => reject(new Error(message.error || "\u767B\u5F55\u5931\u8D25\uFF0C\u672A\u53D6\u5F97\u6388\u6743\u7801\u3002")));
97
+ settle(() => reject(new Error(AUTH_ERROR_HINTS[message.error ?? ""] ?? message.error ?? "\u767B\u5F55\u5931\u8D25\uFF0C\u672A\u53D6\u5F97\u6388\u6743\u7801\u3002")));
92
98
  return;
93
99
  }
100
+ closePopup();
94
101
  client.json("POST", "/api/auth/sdk/token", {
95
102
  code: message.code,
96
103
  state,
@@ -174,6 +181,24 @@ var AuthResource = class {
174
181
  login(options) {
175
182
  return this.client.login(options);
176
183
  }
184
+ /**
185
+ * 用一次性 code 换 PAT。给**服务端**用:浏览器把 code 交给你自己的后端,
186
+ * 后端换到的 PAT 不进浏览器。浏览器里直接用 `login()` 就够了。
187
+ *
188
+ * 服务端调用时会自己补上 `Origin` 头(浏览器会自动带,服务端不会,缺了
189
+ * 后端报 `missing_request_origin`)。
190
+ */
191
+ exchangeCode(params) {
192
+ return this.client.jsonFromInit("/api/auth/sdk/token", {
193
+ method: "POST",
194
+ headers: { Origin: params.clientOrigin },
195
+ body: JSON.stringify({
196
+ code: params.code,
197
+ state: params.state,
198
+ client_origin: params.clientOrigin
199
+ })
200
+ });
201
+ }
177
202
  /** 清除 login() 存储的令牌。 */
178
203
  logoutToken() {
179
204
  this.client.logoutToken();
@@ -407,6 +432,47 @@ var ModelsResource = class {
407
432
  }
408
433
  };
409
434
 
435
+ // src/schemas/session.ts
436
+ import { type } from "arktype";
437
+ var SessionStatus = type(
438
+ "'created' | 'running' | 'completed' | 'failed' | 'interrupted' | 'waiting_for_input'"
439
+ );
440
+ var DEFAULT_REPLAY_SPEED = 5;
441
+ var SessionInfo = type({
442
+ id: "string",
443
+ intent: "string",
444
+ status: SessionStatus,
445
+ created_at: "string",
446
+ updated_at: "string",
447
+ "shared?": "boolean",
448
+ "memory_enabled?": "boolean",
449
+ "is_persistent?": "boolean",
450
+ "is_headless?": "boolean",
451
+ "viewer_role?": "'owner' | 'viewer'",
452
+ "template_id?": "string | null",
453
+ "model?": "string | null",
454
+ "enable_thinking?": "boolean | null",
455
+ "solution_id?": "string | null",
456
+ "solution_ref?": "unknown",
457
+ "biz_role_id?": "string | null",
458
+ "solution?": "unknown",
459
+ "plan_summary?": "string | null",
460
+ "primary_skill_id?": "string | null",
461
+ // ship-attack v2:后端 engine.set_bound_skill 写入的"绑定 skill id"。
462
+ // 未绑定 / 非 ship-attack session 为 null。
463
+ "bound_skill_id?": "string | null",
464
+ "replay_state?": "unknown",
465
+ "is_pinned?": "boolean",
466
+ "pinned_at?": "string | null",
467
+ "is_example?": "boolean",
468
+ "ports?": "unknown",
469
+ "disable_tools?": "string[]",
470
+ "runtime_type?": "string | null",
471
+ "daemon_id?": "string | null",
472
+ "workspace_path?": "string | null",
473
+ "match?": "unknown"
474
+ });
475
+
410
476
  // src/session/definition.ts
411
477
  var SessionSetupError = class extends Error {
412
478
  constructor(message, stage, sessionId, options) {
@@ -827,6 +893,28 @@ function sourceLoopFor(loopId, loopDescriptions) {
827
893
  if (!description) return null;
828
894
  return { loop_name: loopId, description };
829
895
  }
896
+ var MAX_FINAL_ARTIFACTS = 3;
897
+ function normalizeFinalArtifacts(raw) {
898
+ if (!Array.isArray(raw)) return [];
899
+ const artifacts = [];
900
+ const seen = /* @__PURE__ */ new Set();
901
+ for (const item of raw) {
902
+ if (typeof item !== "object" || item === null) continue;
903
+ const record = item;
904
+ const target = String(record.target ?? "").trim();
905
+ if (!target || seen.has(target)) continue;
906
+ const isLink = /^https?:\/\//i.test(target);
907
+ if (!isLink && target.split("/").includes("..")) continue;
908
+ seen.add(target);
909
+ artifacts.push({
910
+ kind: isLink ? "link" : "file",
911
+ target,
912
+ label: String(record.label ?? "").trim()
913
+ });
914
+ if (artifacts.length === MAX_FINAL_ARTIFACTS) break;
915
+ }
916
+ return artifacts;
917
+ }
830
918
  function normalizePostChatFollowup(data, assistantEntryId) {
831
919
  const raw = data.suggestions;
832
920
  const suggestions = [];
@@ -838,11 +926,13 @@ function normalizePostChatFollowup(data, assistantEntryId) {
838
926
  }
839
927
  }
840
928
  const recaption = String(data.recaption ?? "");
841
- if (suggestions.length === 0 && !recaption) return null;
929
+ const finalArtifacts = normalizeFinalArtifacts(data.final_artifacts);
930
+ if (suggestions.length === 0 && !recaption && finalArtifacts.length === 0) return null;
842
931
  return {
843
932
  assistant_entry_id: assistantEntryId,
844
933
  recaption,
845
- suggestions: suggestions.slice(0, 3)
934
+ suggestions: suggestions.slice(0, 3),
935
+ final_artifacts: finalArtifacts
846
936
  };
847
937
  }
848
938
  function normalizeChildPause(pausePayload) {
@@ -1673,7 +1763,7 @@ function projectHistory(entries) {
1673
1763
  loop_id: loopId,
1674
1764
  kind: "message",
1675
1765
  role,
1676
- status: message._error ? "failed" : "completed",
1766
+ status: message._error ? "failed" : message._interrupted ? "interrupted" : "completed",
1677
1767
  blocks,
1678
1768
  tool_calls: toolCalls,
1679
1769
  model: stringOrNull(message.model),
@@ -2130,12 +2220,16 @@ var SessionsResource = class {
2130
2220
  pinSession(sessionId, pinned) {
2131
2221
  return this.client.json("PATCH", `/api/sessions/${sessionId}/pin`, { pinned });
2132
2222
  }
2133
- startReplaySession(sourceSessionId, speed = 5) {
2223
+ startReplaySession(sourceSessionId, speed = DEFAULT_REPLAY_SPEED) {
2134
2224
  return this.client.json("POST", `/api/sessions/${sourceSessionId}/replay`, { speed });
2135
2225
  }
2136
2226
  updateReplaySession(sessionId, payload) {
2137
2227
  return this.client.json("PATCH", `/api/sessions/${sessionId}/replay`, payload);
2138
2228
  }
2229
+ /** 查询会话能否作为回放来源;不支持时 reason 可直接展示,用来提前禁用入口。 */
2230
+ getReplayPreview(sessionId) {
2231
+ return this.client.json("GET", `/api/sessions/${sessionId}/replay/preview`);
2232
+ }
2139
2233
  updateSharing(sessionId, shared) {
2140
2234
  return this.client.json("PATCH", `/api/sessions/${sessionId}/sharing`, { shared });
2141
2235
  }
@@ -2589,10 +2683,15 @@ function contentPreview(content, maxLen = 80) {
2589
2683
  const text = getTextContent(content).trim();
2590
2684
  return text.length > maxLen ? `${text.slice(0, maxLen)}\u2026` : text;
2591
2685
  }
2592
- function transformSlashCommand(skillName, rawInput) {
2686
+ function transformSlashCommand(skillName, rawInput, { local = true, installed = true } = {}) {
2593
2687
  const prompt = rawInput.replace(`<skill>${skillName}</skill>`, "").trim();
2594
- return prompt ? `\u8BF7\u4F7F\u7528 ${skillName} skill \u5B8C\u6210\u4EFB\u52A1
2688
+ if (local) {
2689
+ return prompt ? `\u8BF7\u4F7F\u7528 ${skillName} skill \u5B8C\u6210\u4EFB\u52A1
2595
2690
  ${prompt}` : `\u8BF7\u4F7F\u7528 ${skillName} skill \u5B8C\u6210\u4EFB\u52A1`;
2691
+ }
2692
+ const steps = installed ? `\u8BF7\u5148\u8FD0\u884C blade hub skill use ${skillName} \u52A0\u8F7D\u6280\u80FD` : `\u8BE5\u6280\u80FD\u5C1A\u672A\u5B89\u88C5\u3002\u8BF7\u5148\u8FD0\u884C blade hub skill install ${skillName} \u5B89\u88C5\uFF0C\u518D\u8FD0\u884C blade hub skill use ${skillName} \u52A0\u8F7D`;
2693
+ return prompt ? `${steps}\uFF0C\u7136\u540E\u5B8C\u6210\u4EFB\u52A1\uFF1A
2694
+ ${prompt}` : `${steps}\uFF0C\u7136\u540E\u4F7F\u7528\u5B83\u5B8C\u6210\u4EFB\u52A1`;
2596
2695
  }
2597
2696
  var ATTACHMENT_TAG_RE = /\[附件:\s*([^\]]+)\](?:[ \t]*已上传到工作区路径:[ \t]*([^\r\n]+))?/g;
2598
2697
  var CONTEXT_PART_RE = /^\[上下文:\s*([^\]]+)\]\n([\s\S]*)$/;
@@ -2678,6 +2777,14 @@ function groupMessagesByLoop(messages) {
2678
2777
  }
2679
2778
 
2680
2779
  // src/session/state.ts
2780
+ function toReplaySnapshot(raw) {
2781
+ if (!raw || !raw.status) return null;
2782
+ return {
2783
+ isReplay: raw.status === "replay",
2784
+ speed: raw.speed ?? 1,
2785
+ sourceSessionId: raw.source_session_id ?? null
2786
+ };
2787
+ }
2681
2788
  function createInitialSessionState(sessionId) {
2682
2789
  return {
2683
2790
  sessionId,
@@ -2690,7 +2797,9 @@ function createInitialSessionState(sessionId) {
2690
2797
  errorMessage: null,
2691
2798
  askAnswers: {},
2692
2799
  agentLoops: {},
2693
- activeCompaction: null
2800
+ activeCompaction: null,
2801
+ replay: null,
2802
+ viewerRole: null
2694
2803
  };
2695
2804
  }
2696
2805
  function parseAgentDescription(argumentsJson) {
@@ -3141,6 +3250,11 @@ var AgentSession = class _AgentSession {
3141
3250
  patchFlushCancel = null;
3142
3251
  joined = false;
3143
3252
  joinPending = null;
3253
+ // 回放控制请求排成一条队列。连着点"5x"再点"退出回放"时,并发发出去的两个
3254
+ // 请求响应可能乱序回来,晚到的旧值会把已经退出的会话又显示成回放中;用
3255
+ // 序号丢弃过期响应又会在失败时留下中间态。串行执行就没有这些情况:每个
3256
+ // 响应都是当时最新的,失败也只影响自己。
3257
+ replayQueue = Promise.resolve();
3144
3258
  pendingReplayMessage = null;
3145
3259
  pendingReplayMode;
3146
3260
  recentChatEndAt = 0;
@@ -3171,6 +3285,10 @@ var AgentSession = class _AgentSession {
3171
3285
  emitWithAck: async () => readonlyError(),
3172
3286
  ensureConnected: readonlyError,
3173
3287
  fetchTurns: async () => turns,
3288
+ // 静态历史没有回放身份可言,给个空详情即可,不要抛错——_bootstrap
3289
+ // 不会跑到这里,但保持只读语义一致。
3290
+ fetchSessionInfo: async () => ({ replay_state: null }),
3291
+ updateReplay: async () => readonlyError(),
3174
3292
  listDir: async () => readonlyError(),
3175
3293
  countFiles: async () => readonlyError(),
3176
3294
  downloadFile: async () => readonlyError(),
@@ -3462,10 +3580,13 @@ var AgentSession = class _AgentSession {
3462
3580
  this.joinPending = null;
3463
3581
  try {
3464
3582
  await this.ensureJoined();
3465
- await this.syncTurnsFromHistory({
3466
- clearOnEmpty: true,
3467
- preserveChangesSince: turnsBeforeRejoin
3468
- });
3583
+ await Promise.all([
3584
+ this.syncTurnsFromHistory({
3585
+ clearOnEmpty: true,
3586
+ preserveChangesSince: turnsBeforeRejoin
3587
+ }),
3588
+ this.syncReplayFromSession()
3589
+ ]);
3469
3590
  } catch {
3470
3591
  }
3471
3592
  }
@@ -3473,6 +3594,40 @@ var AgentSession = class _AgentSession {
3473
3594
  async _bootstrap() {
3474
3595
  await this.syncTurnsFromHistory({ suppressErrors: false });
3475
3596
  await this.ensureJoined();
3597
+ await this.syncReplayFromSession();
3598
+ }
3599
+ /** 改回放是 owner 专属(服务端 PATCH 走 _get_owned_session)。 */
3600
+ get canControlReplay() {
3601
+ return this.state.viewerRole !== "viewer";
3602
+ }
3603
+ /** 拉一次会话详情,填回放状态和只读身份;失败按非回放会话处理,不挡启动。 */
3604
+ async syncReplayFromSession() {
3605
+ try {
3606
+ const info = await this.runtime.fetchSessionInfo(this.sessionId);
3607
+ const replay = toReplaySnapshot(info.replay_state);
3608
+ const viewerRole = info.viewer_role ?? null;
3609
+ this.update((s) => ({ ...s, replay, viewerRole }));
3610
+ } catch {
3611
+ }
3612
+ }
3613
+ /** 调整回放倍速;不是回放会话、或只读身份时什么都不做。 */
3614
+ async setReplaySpeed(speed) {
3615
+ if (!this.state.replay?.isReplay || !this.canControlReplay) return;
3616
+ await this.applyReplayControl({ speed });
3617
+ }
3618
+ /** 退出回放,之后的对话交给真实模型运行;不是回放会话、或只读身份时什么都不做。 */
3619
+ async exitReplay() {
3620
+ if (!this.state.replay?.isReplay || !this.canControlReplay) return;
3621
+ await this.applyReplayControl({ status: "autonomous" });
3622
+ }
3623
+ /** 把一次回放控制请求排进队列;前一个完成后才发出,响应直接安装。 */
3624
+ applyReplayControl(payload) {
3625
+ const run = this.replayQueue.then(async () => {
3626
+ const result = await this.runtime.updateReplay(this.sessionId, payload);
3627
+ this.update((s) => ({ ...s, replay: toReplaySnapshot(result) }));
3628
+ });
3629
+ this.replayQueue = run.catch(() => void 0);
3630
+ return run;
3476
3631
  }
3477
3632
  /** @internal */
3478
3633
  _handleTurnStart(turn) {
@@ -3552,6 +3707,10 @@ var AgentSession = class _AgentSession {
3552
3707
  if (data.status) {
3553
3708
  this.update((s) => ({ ...s, status: data.status ?? s.status }));
3554
3709
  }
3710
+ if (data.replay_state !== void 0) {
3711
+ const replay = toReplaySnapshot(data.replay_state);
3712
+ this.update((s) => ({ ...s, replay }));
3713
+ }
3555
3714
  this.emitter.emit("sessionUpdated", {
3556
3715
  intent: data.intent,
3557
3716
  status: data.status,
@@ -4001,6 +4160,8 @@ var SessionHub = class {
4001
4160
  }),
4002
4161
  ensureConnected: () => this.ensureConnected(),
4003
4162
  fetchTurns: (sessionId) => this.client.sessions.getSessionTurns(sessionId),
4163
+ fetchSessionInfo: (sessionId) => this.client.sessions.getSession(sessionId),
4164
+ updateReplay: (sessionId, payload) => this.client.sessions.updateReplaySession(sessionId, payload).then((result) => result.replay_state ?? null),
4004
4165
  listDir: (sessionId, dirPath) => this.client.sessions.listDir(sessionId, dirPath),
4005
4166
  countFiles: (sessionId, dirPath) => this.client.sessions.countFiles(sessionId, dirPath),
4006
4167
  downloadFile: (sessionId, filePath, downloadName) => this.client.sessions.downloadFile(sessionId, filePath, downloadName),
@@ -4222,7 +4383,7 @@ function resolveAuthToken(options) {
4222
4383
 
4223
4384
  // src/version.ts
4224
4385
  var SDK_NAME = "agent-client";
4225
- var SDK_VERSION = true ? "2608.0.2" : "1.1.1";
4386
+ var SDK_VERSION = true ? "2608.0.3" : "1.1.1";
4226
4387
 
4227
4388
  // src/socket.ts
4228
4389
  function withSdkIdentity(auth) {
@@ -4674,46 +4835,6 @@ function connectEmbedded(options) {
4674
4835
  };
4675
4836
  }
4676
4837
 
4677
- // src/schemas/session.ts
4678
- import { type } from "arktype";
4679
- var SessionStatus = type(
4680
- "'created' | 'running' | 'completed' | 'failed' | 'interrupted' | 'waiting_for_input'"
4681
- );
4682
- var SessionInfo = type({
4683
- id: "string",
4684
- intent: "string",
4685
- status: SessionStatus,
4686
- created_at: "string",
4687
- updated_at: "string",
4688
- "shared?": "boolean",
4689
- "memory_enabled?": "boolean",
4690
- "is_persistent?": "boolean",
4691
- "is_headless?": "boolean",
4692
- "viewer_role?": "'owner' | 'viewer'",
4693
- "template_id?": "string | null",
4694
- "model?": "string | null",
4695
- "enable_thinking?": "boolean | null",
4696
- "solution_id?": "string | null",
4697
- "solution_ref?": "unknown",
4698
- "biz_role_id?": "string | null",
4699
- "solution?": "unknown",
4700
- "plan_summary?": "string | null",
4701
- "primary_skill_id?": "string | null",
4702
- // ship-attack v2:后端 engine.set_bound_skill 写入的"绑定 skill id"。
4703
- // 未绑定 / 非 ship-attack session 为 null。
4704
- "bound_skill_id?": "string | null",
4705
- "replay_state?": "unknown",
4706
- "is_pinned?": "boolean",
4707
- "pinned_at?": "string | null",
4708
- "is_example?": "boolean",
4709
- "ports?": "unknown",
4710
- "disable_tools?": "string[]",
4711
- "runtime_type?": "string | null",
4712
- "daemon_id?": "string | null",
4713
- "workspace_path?": "string | null",
4714
- "match?": "unknown"
4715
- });
4716
-
4717
4838
  // src/schemas/solution.ts
4718
4839
  var LayoutType = /* @__PURE__ */ ((LayoutType2) => {
4719
4840
  LayoutType2["Default"] = "default";
@@ -4745,6 +4866,7 @@ export {
4745
4866
  BladeApiError,
4746
4867
  BladeClient,
4747
4868
  ClientProjectionBuilder,
4869
+ DEFAULT_REPLAY_SPEED,
4748
4870
  LayoutType,
4749
4871
  ModelsResource,
4750
4872
  SDK_NAME,
@@ -4769,6 +4891,7 @@ export {
4769
4891
  isHiddenInternalMessage,
4770
4892
  isInboundEnvelope,
4771
4893
  normalizeMessageContent,
4894
+ toReplaySnapshot,
4772
4895
  transformSlashCommand
4773
4896
  };
4774
4897
  //# sourceMappingURL=index.js.map