@honor-claw/yoyo 2026.7.1-beta.21 → 2026.7.1-beta.22

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.
@@ -1,38 +1,46 @@
1
- import { existsSync as e, readdirSync as t } from "node:fs";
2
- import n from "node:path";
3
- import r from "node:os";
4
- import { assertAbsolutePathInput as i, canonicalPathFromExistingAncestor as a, ensureAbsoluteDirectory as o, resolveAbsolutePathForRead as s, resolveAbsolutePathForWrite as c, sanitizeUntrustedFileName as l } from "openclaw/plugin-sdk/security-runtime";
1
+ import { toErrorMessage as e } from "../../utils/error.mjs";
2
+ import { clawLogger as t } from "../../utils/claw-logger.mjs";
3
+ import { existsSync as n, readdirSync as r } from "node:fs";
4
+ import i from "node:path";
5
+ import a from "node:os";
6
+ import { canonicalPathFromExistingAncestor as o, ensureAbsoluteDirectory as s, resolveAbsolutePathForRead as c, resolveAbsolutePathForWrite as l, sanitizeUntrustedFileName as u } from "openclaw/plugin-sdk/security-runtime";
5
7
  //#region src/capabilities/remote-file/paths.ts
6
- var u = n.join(r.homedir(), ".openclaw", "downloads");
7
- async function d(e) {
8
+ var d = t("capabilities");
9
+ function f(e) {
10
+ if (!e) throw Error("path is required");
11
+ if (e.includes("\0")) throw Error("path must not contain NUL bytes");
12
+ if (!i.isAbsolute(e)) throw Error("path must be absolute");
13
+ }
14
+ var p = i.join(a.homedir(), ".openclaw", "downloads");
15
+ async function m(t) {
8
16
  try {
9
- return i(e), { absolutePath: await a((await s(e)).path) };
10
- } catch {
11
- return {
17
+ return f(t), { absolutePath: await o((await c(t)).path) };
18
+ } catch (n) {
19
+ return d.error("resolve read path failed", n, { localPath: t }), {
12
20
  error: !0,
13
- message: "文件不存在或不可读"
21
+ message: `文件不存在或不可读: ${e(n)}`
14
22
  };
15
23
  }
16
24
  }
17
- async function f(t, n) {
18
- let r = t && t.trim().length > 0 ? t : u, a;
25
+ async function h(e, t) {
26
+ let r = e && e.trim().length > 0 ? e : p, i;
19
27
  try {
20
- i(r), a = (await c(r)).path;
28
+ f(r), i = (await l(r)).path;
21
29
  } catch {
22
30
  return {
23
31
  ok: !1,
24
32
  message: "目标目录路径格式不正确"
25
33
  };
26
34
  }
27
- if (n === !1) return e(a) ? {
35
+ if (t === !1) return n(i) ? {
28
36
  ok: !0,
29
- dir: a
37
+ dir: i
30
38
  } : {
31
39
  ok: !1,
32
40
  message: "目标目录不存在"
33
41
  };
34
42
  try {
35
- let e = await o(a);
43
+ let e = await s(i);
36
44
  return e.ok ? {
37
45
  ok: !0,
38
46
  dir: e.path
@@ -47,19 +55,19 @@ async function f(t, n) {
47
55
  };
48
56
  }
49
57
  }
50
- function p(e, t) {
51
- let r = l(e, "download"), i = n.extname(r), a = i ? r.slice(0, -i.length) : r;
52
- if (!t.has(r)) return r;
58
+ function g(e, t) {
59
+ let n = u(e, "download"), r = i.extname(n), a = r ? n.slice(0, -r.length) : n;
60
+ if (!t.has(n)) return n;
53
61
  let o = 1;
54
- for (; t.has(`${a} (${o})${i}`);) o++;
55
- return `${a} (${o})${i}`;
62
+ for (; t.has(`${a} (${o})${r}`);) o++;
63
+ return `${a} (${o})${r}`;
56
64
  }
57
- function m(e) {
65
+ function _(e) {
58
66
  try {
59
- return new Set(t(e));
67
+ return new Set(r(e));
60
68
  } catch {
61
69
  return /* @__PURE__ */ new Set();
62
70
  }
63
71
  }
64
72
  //#endregion
65
- export { u as DEFAULT_DOWNLOAD_DIR, p as nextAvailableName, f as prepareSaveDir, m as readExistingNames, d as resolveReadPath };
73
+ export { p as DEFAULT_DOWNLOAD_DIR, g as nextAvailableName, h as prepareSaveDir, _ as readExistingNames, m as resolveReadPath };
@@ -7,14 +7,14 @@ import { normalizeAnswerPartial as a, normalizeFinalVisibleText as o } from "../
7
7
  import "../../guard/index.mjs";
8
8
  import { finishThinking as s, pushStreamDelta as c, pushThinkingDelta as l, pushToolEvent as u, pushToolStatus as d } from "./session.mjs";
9
9
  import { captureErrorPayload as f } from "./error.mjs";
10
- import { resolveAnswerPartialDelta as p, resolveThinkingDelta as m } from "./resolvers.mjs";
11
- import { buildToolResultEvent as h, buildToolStartEvent as g } from "./tool/event-builder.mjs";
10
+ import { resolveAnswerPartialDelta as p, resolveThinkingDelta as m, rotateAnswerSegment as h } from "./resolvers.mjs";
11
+ import { buildToolResultEvent as g, buildToolStartEvent as _ } from "./tool/event-builder.mjs";
12
12
  //#region src/messaging/dispatchers/stream/controller.ts
13
- var _ = t("dispatcher"), v = "💬";
14
- function y(e, t) {
13
+ var v = t("dispatcher"), y = "💬";
14
+ function b(e, t) {
15
15
  return e ?? `no-id:${t}`;
16
16
  }
17
- var b = class {
17
+ var x = class {
18
18
  session;
19
19
  includeThinking;
20
20
  includeToolCalls;
@@ -52,8 +52,8 @@ var b = class {
52
52
  }
53
53
  emitToolStart(e) {
54
54
  if (!this.includeToolCalls || !e.name) return;
55
- let t = y(e.toolCallId, e.name);
56
- this.emittedToolStartKeys.has(t) || (this.emittedToolStartKeys.add(t), u(this.session, g(e)), this.pendingToolKeys.add(t), e.toolCallId && this.flushPendingToolStatus(e.toolCallId), _.debug("tool start emitted", {
55
+ let t = b(e.toolCallId, e.name);
56
+ this.emittedToolStartKeys.has(t) || (this.emittedToolStartKeys.add(t), u(this.session, _(e)), this.pendingToolKeys.add(t), e.toolCallId && this.flushPendingToolStatus(e.toolCallId), v.debug("tool start emitted", {
57
57
  sessionKey: this.session.sessionKey,
58
58
  toolName: e.name,
59
59
  hasToolCallId: !!e.toolCallId,
@@ -61,7 +61,7 @@ var b = class {
61
61
  }));
62
62
  }
63
63
  emitToolResult(e) {
64
- this.includeToolCalls && (u(this.session, h(e)), this.pendingToolKeys.delete(y(e.toolCallId, e.toolName)), e.toolCallId && this.pendingToolStatus.delete(e.toolCallId), this.pendingToolKeys.size === 0 && this.flushPendingReplyPayloads());
64
+ this.includeToolCalls && (u(this.session, g(e)), this.pendingToolKeys.delete(b(e.toolCallId, e.toolName)), e.toolCallId && this.pendingToolStatus.delete(e.toolCallId), this.pendingToolKeys.size === 0 && this.flushPendingReplyPayloads());
65
65
  }
66
66
  onToolStart = (e) => {
67
67
  e.phase && e.phase !== "start" || e.name && this.emitToolStart({
@@ -76,7 +76,7 @@ var b = class {
76
76
  this.emitToolResult(t);
77
77
  return;
78
78
  }
79
- _.warn("tool result frame skipped, no finished step", {
79
+ v.warn("tool result frame skipped, no finished step", {
80
80
  sessionKey: this.session.sessionKey,
81
81
  chatId: this.session.chatId,
82
82
  hasToolCallId: !!e,
@@ -132,25 +132,32 @@ var b = class {
132
132
  this.includeThinking && s(this.session);
133
133
  };
134
134
  rescueCommentaryProgress(e) {
135
- if (typeof e != "string" || !e.startsWith(v)) return;
135
+ if (typeof e != "string" || !e.startsWith(y)) return;
136
136
  let t = e.slice(2).trimStart(), n = this.session.answerSnapshot;
137
137
  if (!t || n.startsWith(t)) return;
138
138
  if (this.pendingReplyPayloads.length > 0) {
139
- _.warn("commentary rescue skipped: pending reply payloads", {
139
+ v.warn("commentary rescue skipped: pending reply payloads", {
140
140
  sessionKey: this.session.sessionKey,
141
141
  pending: this.pendingReplyPayloads.length
142
142
  });
143
143
  return;
144
144
  }
145
145
  let r = t.startsWith(n) ? t.slice(n.length) : t;
146
- this.session.answerSnapshot = t, c(this.session, r);
146
+ h(this.session, t), c(this.session, r);
147
147
  }
148
148
  async pushFinalAnswerDiff(e) {
149
149
  if (!e) return;
150
- let t = this.session.answerSnapshot;
151
- if (t.startsWith(e)) return;
152
- let n = e.startsWith(t) ? e.slice(t.length) : e;
153
- n && (this.session.answerSnapshot = e, await c(this.session, n));
150
+ let t = 0;
151
+ for (let n of [this.session.answerSnapshot, this.session.prevAnswerSnapshot]) if (n) {
152
+ if (n.startsWith(e)) return;
153
+ for (let r of [n, n.trim()]) r && e.startsWith(r) && r.length > t && (t = r.length);
154
+ }
155
+ t >= e.length || (t === 0 && v.debug("final diff full push, no covered segment", {
156
+ sessionKey: this.session.sessionKey,
157
+ textLen: e.length,
158
+ snapshotLen: this.session.answerSnapshot.length,
159
+ prevLen: this.session.prevAnswerSnapshot.length
160
+ }), h(this.session, e), await c(this.session, e.slice(t)));
154
161
  }
155
162
  deliver = async (e, t) => {
156
163
  if (t.kind === "tool") {
@@ -167,7 +174,7 @@ var b = class {
167
174
  }
168
175
  };
169
176
  onError = (t) => {
170
- n(this.session.chatId, "reply_error", { error: e(t) }), _.warn("reply dispatcher error", {
177
+ n(this.session.chatId, "reply_error", { error: e(t) }), v.warn("reply dispatcher error", {
171
178
  sender: this.session.target.deviceId,
172
179
  turnId: this.session.traceInfo.turnId,
173
180
  error: t
@@ -201,4 +208,4 @@ var b = class {
201
208
  }
202
209
  };
203
210
  //#endregion
204
- export { b as StreamingController };
211
+ export { x as StreamingController };
@@ -4,21 +4,24 @@ var t = e("dispatcher");
4
4
  function n(e, t) {
5
5
  return t.length < e.length || !t.startsWith(e);
6
6
  }
7
- function r(e, r) {
8
- if (r.delta) return typeof r.text == "string" ? e.answerSnapshot = r.text : e.answerSnapshot += r.delta, r.delta;
9
- if (typeof r.text != "string" || r.text.length === 0) return "";
10
- if (r.replace) return t.debug("answer partial dropped: replace frame", {
7
+ function r(e, t) {
8
+ e.prevAnswerSnapshot = e.answerSnapshot, e.answerSnapshot = t;
9
+ }
10
+ function i(e, i) {
11
+ if (i.delta) return typeof i.text == "string" ? e.answerSnapshot = i.text : e.answerSnapshot += i.delta, i.delta;
12
+ if (typeof i.text != "string" || i.text.length === 0) return "";
13
+ if (i.replace) return t.debug("answer partial dropped: replace frame", {
11
14
  sessionKey: e.sessionKey,
12
15
  chatId: e.chatId,
13
16
  snapshotLen: e.answerSnapshot.length,
14
- textLen: r.text.length,
15
- textHead: r.text.slice(0, 16)
16
- }), e.answerSnapshot = r.text, "";
17
- if (n(e.answerSnapshot, r.text)) return e.answerSnapshot = r.text, r.text;
18
- let i = r.text.slice(e.answerSnapshot.length);
19
- return e.answerSnapshot = r.text, i;
17
+ textLen: i.text.length,
18
+ textHead: i.text.slice(0, 16)
19
+ }), r(e, i.text), "";
20
+ if (n(e.answerSnapshot, i.text)) return r(e, i.text), i.text;
21
+ let a = i.text.slice(e.answerSnapshot.length);
22
+ return e.answerSnapshot = i.text, a;
20
23
  }
21
- function i(e, t) {
24
+ function a(e, t) {
22
25
  let r = t.text ?? "";
23
26
  if (!r) return "";
24
27
  if (t.isReasoningSnapshot) {
@@ -29,4 +32,4 @@ function i(e, t) {
29
32
  return e.thinkingSnapshot += r, r;
30
33
  }
31
34
  //#endregion
32
- export { r as resolveAnswerPartialDelta, i as resolveThinkingDelta };
35
+ export { i as resolveAnswerPartialDelta, a as resolveThinkingDelta, r as rotateAnswerSegment };
@@ -26,6 +26,7 @@ function c(e) {
26
26
  silent: e.silent ?? !1,
27
27
  contentGuard: r(),
28
28
  answerSnapshot: "",
29
+ prevAnswerSnapshot: "",
29
30
  thinkingSnapshot: "",
30
31
  hasPushedAnswerText: !1,
31
32
  hasPushedThinkingText: !1
@@ -1,9 +1,12 @@
1
- import { resolveReadPath as e } from "../../capabilities/remote-file/paths.mjs";
1
+ import { toErrorMessage as e } from "../../utils/error.mjs";
2
+ import { clawLogger as t } from "../../utils/claw-logger.mjs";
3
+ import { resolveReadPath as n } from "../../capabilities/remote-file/paths.mjs";
2
4
  import "../../capabilities/remote-file/index.mjs";
3
- import t from "node:path";
4
- import { stat as n } from "node:fs/promises";
5
+ import r from "node:path";
6
+ import { stat as i } from "node:fs/promises";
5
7
  //#region src/tools/remote-file-upload/helpers.ts
6
- function r(e, t) {
8
+ var a = t("tools");
9
+ function o(e, t) {
7
10
  return new Promise((n, r) => {
8
11
  if (t?.aborted) {
9
12
  r(/* @__PURE__ */ Error("aborted"));
@@ -17,48 +20,48 @@ function r(e, t) {
17
20
  t?.addEventListener("abort", a, { once: !0 });
18
21
  });
19
22
  }
20
- function i(e) {
23
+ function s(e) {
21
24
  return !!e.errorInfo;
22
25
  }
23
- async function a(r) {
24
- let i = [], a = [];
25
- return await Promise.all(r.map(async (r, o) => {
26
- let s = await e(r);
27
- if ("error" in s) {
28
- a.push({
29
- index: o,
30
- fileName: t.basename(r) || r,
31
- reason: s.message
26
+ async function c(t) {
27
+ let o = [], s = [];
28
+ return await Promise.all(t.map(async (t, c) => {
29
+ let l = await n(t);
30
+ if ("error" in l) {
31
+ s.push({
32
+ index: c,
33
+ fileName: r.basename(t) || t,
34
+ reason: l.message
32
35
  });
33
36
  return;
34
37
  }
35
38
  try {
36
- let e = await n(s.absolutePath);
39
+ let e = await i(l.absolutePath);
37
40
  if (!e.isFile()) {
38
- a.push({
39
- index: o,
40
- fileName: t.basename(s.absolutePath),
41
+ s.push({
42
+ index: c,
43
+ fileName: r.basename(l.absolutePath),
41
44
  reason: "文件不存在或不可读"
42
45
  });
43
46
  return;
44
47
  }
45
- i.push({
46
- index: o,
47
- fileName: t.basename(s.absolutePath),
48
+ o.push({
49
+ index: c,
50
+ fileName: r.basename(l.absolutePath),
48
51
  fileSize: e.size,
49
- absolutePath: s.absolutePath
52
+ absolutePath: l.absolutePath
50
53
  });
51
- } catch {
52
- a.push({
53
- index: o,
54
- fileName: t.basename(s.absolutePath),
55
- reason: "文件不存在或不可读"
54
+ } catch (t) {
55
+ a.error("preflight stat failed", t, { localPath: l.absolutePath }), s.push({
56
+ index: c,
57
+ fileName: r.basename(l.absolutePath),
58
+ reason: `文件不存在或不可读: ${e(t)}`
56
59
  });
57
60
  }
58
- })), i.sort((e, t) => e.index - t.index), {
59
- ok: i,
60
- failed: a
61
+ })), o.sort((e, t) => e.index - t.index), {
62
+ ok: o,
63
+ failed: s
61
64
  };
62
65
  }
63
66
  //#endregion
64
- export { i as isErrorItem, a as preflight, r as sleep };
67
+ export { s as isErrorItem, c as preflight, o as sleep };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@honor-claw/yoyo",
3
- "version": "2026.7.1-beta.21",
3
+ "version": "2026.7.1-beta.22",
4
4
  "description": "OpenClaw Honor Yoyo connection plugin",
5
5
  "keywords": [
6
6
  "ai",
@@ -38,7 +38,7 @@ metadata: { "openclaw": { "emoji": "📤", "always": false } }
38
38
 
39
39
  **已知传哪个文件(路径齐全)+ 目标设备唯一(单台或用户已指定)→ 直接传,不确认、不寒暄。**仅以下信息不全 / 有歧义才追问:
40
40
 
41
- > 设备数量不是 1:多台 connected 且用户未指定 → 按下表「多设备候选」追问;无 connected 设备 → 告知"当前无设备连接",止步,不得调上传/下载。
41
+ > 设备数量不是 1:多台 connected 且用户未指定 → 按下表「多设备候选」追问;无 connected 设备 → 告知"当前无设备连接",止步,不得调上传/下载。该校验仅针对需 node_invoke 的远端设备(手机);PC 为本机直调,不作节点校验。
42
42
 
43
43
  | 场景 | 话术 |
44
44
  |---|---|
@@ -52,11 +52,7 @@ metadata: { "openclaw": { "emoji": "📤", "always": false } }
52
52
 
53
53
  - 收尾(用户取消时用一句话,其余场景一律走「Summary 模板」表格):用户取消 → 已取消本次传输。
54
54
 
55
- **发提醒的时机**(上传、下载各提醒一次,不刷屏):
56
-
57
- - **上传提醒**:设备已定向(`nodes_status` 返回后)+ 路径齐全,**调上传工具的同一条消息里先输出提醒、再调工具**——用户在传输开始时看到;轮询/执行过程中不再重复。
58
- - **下载提醒**:上传成功后,**调下载工具的同一条消息里先输出提醒、再调工具**。上传全失败 → 不调下载、不发下载提醒;跳过上传(已在云侧中转)→ 只发下载提醒。
59
- - 提醒是「执行过程不展示给用户」的**唯一例外**;仍受「`nodes_status` 返回前不得承诺」约束——设备未定向前不发任何提醒。
55
+ **发提醒的时机**(上传、下载各提醒一次,不刷屏):具体时机和话术在各方向 reference 的对应步骤里,以 reference 为准。
60
56
 
61
57
  ## 轮询契约(task-result-query)
62
58
 
@@ -37,6 +37,8 @@ PC 源端用户在 PC。三种情况:
37
37
  - 多个 connected 且无法区分 → 按 SKILL「追问话术·多设备候选」询问用户
38
38
  - 无 connected 设备 → 告知"当前无设备连接",止步,不得调上传
39
39
 
40
+ 此校验仅针对目标手机;源端 PC 即当前会话所在的本机 openclaw,上传走本地直调,**不要求 nodes_status 中存在 connected 的 PC 节点**。
41
+
40
42
  ### 步骤 3:源端上传(PC 直调,同步)
41
43
 
42
44
  **源端 PC:本地直调 `remote_file_upload`,`execute` 同步返回即完成,不轮询。**
@@ -56,7 +58,9 @@ PC 源端用户在 PC。三种情况:
56
58
  | `totalCount` / `successCount` / `failCount` | 总数 / 成功数 / 失败数 |
57
59
  | `errorMessage?` | 整批级异常时 |
58
60
 
59
- 上传出参中成功项的 `fileId`、`accessUrl`、`fileName`、`accessExpireTime` 留作步骤 4 下载入参。**调 `remote_file_upload` 前按 SKILL「提醒话术·时机」输出上传提醒**(同条消息先提醒再调工具;PC 直调同步返回快,提醒仍要在调用前发出)。本方向发送端是 PC(本机 openclaw),上传提醒固定为"发送端(PC)文件上传中,请勿发送新指令,否则会中断传输。"。上传全失败 → 不下载直接报;部分成功 → 只下载成功项。
61
+ 上传出参中成功项的 `fileId`、`accessUrl`、`fileName`、`accessExpireTime` 留作步骤 4 下载入参。上传全失败 → 不下载直接报;部分成功 → 只下载成功项。
62
+
63
+ **上传提醒**:调 `remote_file_upload` 前按 SKILL「提醒话术·时机」输出上传提醒(同条消息先提醒再调工具;PC 直调同步返回快,提醒仍要在调用前发出)。本方向发送端是 PC(本机 openclaw),上传提醒固定为"发送端(PC)文件上传中,请勿发送新指令,否则会中断传输。"。
60
64
 
61
65
  ### 步骤 4:目标端下载(手机 node_invoke + 轮询)
62
66
 
@@ -98,7 +102,9 @@ PC 源端用户在 PC。三种情况:
98
102
  ❌ 错: files: { fileId, accessUrl, fileName, ... } // 裸对象
99
103
  ```
100
104
 
101
- **字段来源(调 `node_invoke` 前)**:`fileId`/`accessUrl`/`fileName`/`accessExpireTime` 原样取步骤 3 upload 出参对应成功项,不得编造/丢失。按 SKILL「提醒话术·时机」先输出下载提醒(同条消息先提醒再调工具),轮询过程中不再重复。本方向目标端是手机,下载提醒为"目标端({目标手机 nodeName})文件下载中,请勿发送新指令,否则会中断传输。",`nodeName` 取步骤 2 中选中目标手机的 `nodeName`。
105
+ **字段来源(调 `node_invoke` 前)**:`fileId`/`accessUrl`/`fileName`/`accessExpireTime` 原样取步骤 3 upload 出参对应成功项,不得编造/丢失。
106
+
107
+ **下载提醒**:按 SKILL「提醒话术·时机」先输出下载提醒(同条消息先提醒再调工具),轮询过程中不再重复。本方向目标端是手机,下载提醒为"目标端({目标手机 nodeName})文件下载中,请勿发送新指令,否则会中断传输。",`nodeName` 取步骤 2 中选中目标手机的 `nodeName`。
102
108
  2. **查进度**:反复 `node_invoke({ nodeId, name:"task-result-query", params:{ task_id } })`,看顶层 `isFinal`——`false` → 继续轮询;`true` → 停,取 `files[]`。**轮询契约(isFinal 判定 / 1-3 秒节奏 / 300 次超限 / 严禁自行推测结论)见 SKILL「轮询契约」,此处不重复**。
103
109
  3. **拿结果**:`isFinal:true` 停止 → `files[]` 每文件终态,顶层带 `successCount`/`failCount`,整批级异常在 `errorMessage`
104
110
 
@@ -29,6 +29,8 @@ description: 手机 → PC 方向的完整执行流程。源端手机 node_invok
29
29
  - 多个 connected 且无法区分 → 询问用户
30
30
  - 无 connected 设备 → 告知"当前无设备连接",止步,不得先调上传
31
31
 
32
+ 此校验仅针对源手机;目标端 PC 即当前会话所在的本机 openclaw,下载走本地直调,**不要求 nodes_status 中存在 connected 的 PC 节点**,不得因 nodes_status 无 PC 节点而中止。
33
+
32
34
  ### 步骤 2:拿源端路径
33
35
 
34
36
  按用户是否给出源手机完整路径,分两支拿 `filePaths`(手机路径形如 `/storage/emulated/0/...`):
@@ -109,7 +111,9 @@ description: 手机 → PC 方向的完整执行流程。源端手机 node_invok
109
111
  | `fileSize` | 文件大小 |
110
112
  | `failReason` | `FAILED` / `CANCELLED` 必带 |
111
113
 
112
- 上传出参中成功项的 `fileId`、`accessUrl`、`fileName` 留作步骤 4 下载入参。**派活 `remote_file_upload` 前按 SKILL「提醒话术·时机」输出上传提醒**(同条消息先提醒再调工具),轮询过程中不再重复。本方向发送端是手机,上传提醒为"发送端({源手机 nodeName})文件上传中,请勿发送新指令,否则会中断传输。",`nodeName` 取步骤 1 中选中源手机的 `nodeName`。上传全失败 → 不下载直接报;部分成功 → 只下载成功项。
114
+ 上传出参中成功项的 `fileId`、`accessUrl`、`fileName` 留作步骤 4 下载入参。上传全失败 → 不下载直接报;部分成功 → 只下载成功项。
115
+
116
+ **上传提醒**:派活 `remote_file_upload` 前按 SKILL「提醒话术·时机」输出上传提醒(同条消息先提醒再调工具),轮询过程中不再重复。本方向发送端是手机,上传提醒为"发送端({源手机 nodeName})文件上传中,请勿发送新指令,否则会中断传输。",`nodeName` 取步骤 1 中选中源手机的 `nodeName`。
113
117
 
114
118
  ### 步骤 4:目标端下载(PC 直调,同步)
115
119
 
@@ -131,7 +135,9 @@ description: 手机 → PC 方向的完整执行流程。源端手机 node_invok
131
135
  | `totalCount` / `successCount` / `failCount` | 总数 / 成功数 / 失败数 |
132
136
  | `errorMessage?` | 整批级异常时 |
133
137
 
134
- **字段来源(调 `remote_file_download` 前)**:`files[]` 每项的 `fileId`/`accessUrl`/`fileName` 均原样取源端 upload 出参对应成功项,不得编造/丢失。按 SKILL「提醒话术·时机」先输出下载提醒(同条消息先提醒再调工具;PC 直调同步返回快,提醒仍要在调用前发出)。本方向目标端是 PC(本机 openclaw),下载提醒固定为"目标端(PC)文件下载中,请勿发送新指令,否则会中断传输。"。
138
+ **字段来源(调 `remote_file_download` 前)**:`files[]` 每项的 `fileId`/`accessUrl`/`fileName` 均原样取源端 upload 出参对应成功项,不得编造/丢失。
139
+
140
+ **下载提醒**:按 SKILL「提醒话术·时机」先输出下载提醒(同条消息先提醒再调工具;PC 直调同步返回快,提醒仍要在调用前发出)。本方向目标端是 PC(本机 openclaw),下载提醒固定为"目标端(PC)文件下载中,请勿发送新指令,否则会中断传输。"。
135
141
 
136
142
  **saveDir**:用户指定 → 传 `saveDir`(PC 插件可创建任意目录);未指定 → 不传,默认 `~/.openclaw/downloads`,不追问保存位置。
137
143