@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.
- package/dist/capabilities/remote-file/paths.mjs +32 -24
- package/dist/messaging/dispatchers/stream/controller.mjs +25 -18
- package/dist/messaging/dispatchers/stream/resolvers.mjs +15 -12
- package/dist/messaging/dispatchers/stream/session.mjs +1 -0
- package/dist/tools/remote-file-upload/helpers.mjs +34 -31
- package/package.json +1 -1
- package/skills/yoyo-file-transfer/SKILL.md +2 -6
- package/skills/yoyo-file-transfer/references/pc-to-phone.md +8 -2
- package/skills/yoyo-file-transfer/references/phone-to-pc.md +8 -2
|
@@ -1,38 +1,46 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import r from "node:
|
|
4
|
-
import
|
|
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
|
|
7
|
-
|
|
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
|
|
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
|
|
18
|
-
let r =
|
|
25
|
+
async function h(e, t) {
|
|
26
|
+
let r = e && e.trim().length > 0 ? e : p, i;
|
|
19
27
|
try {
|
|
20
|
-
|
|
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 (
|
|
35
|
+
if (t === !1) return n(i) ? {
|
|
28
36
|
ok: !0,
|
|
29
|
-
dir:
|
|
37
|
+
dir: i
|
|
30
38
|
} : {
|
|
31
39
|
ok: !1,
|
|
32
40
|
message: "目标目录不存在"
|
|
33
41
|
};
|
|
34
42
|
try {
|
|
35
|
-
let e = await
|
|
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
|
|
51
|
-
let
|
|
52
|
-
if (!t.has(
|
|
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})${
|
|
55
|
-
return `${a} (${o})${
|
|
62
|
+
for (; t.has(`${a} (${o})${r}`);) o++;
|
|
63
|
+
return `${a} (${o})${r}`;
|
|
56
64
|
}
|
|
57
|
-
function
|
|
65
|
+
function _(e) {
|
|
58
66
|
try {
|
|
59
|
-
return new Set(
|
|
67
|
+
return new Set(r(e));
|
|
60
68
|
} catch {
|
|
61
69
|
return /* @__PURE__ */ new Set();
|
|
62
70
|
}
|
|
63
71
|
}
|
|
64
72
|
//#endregion
|
|
65
|
-
export {
|
|
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
|
|
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
|
|
14
|
-
function
|
|
13
|
+
var v = t("dispatcher"), y = "💬";
|
|
14
|
+
function b(e, t) {
|
|
15
15
|
return e ?? `no-id:${t}`;
|
|
16
16
|
}
|
|
17
|
-
var
|
|
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 =
|
|
56
|
-
this.emittedToolStartKeys.has(t) || (this.emittedToolStartKeys.add(t), u(this.session,
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
146
|
+
h(this.session, t), c(this.session, r);
|
|
147
147
|
}
|
|
148
148
|
async pushFinalAnswerDiff(e) {
|
|
149
149
|
if (!e) return;
|
|
150
|
-
let t =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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) }),
|
|
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 {
|
|
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,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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:
|
|
15
|
-
textHead:
|
|
16
|
-
}), e
|
|
17
|
-
if (n(e.answerSnapshot,
|
|
18
|
-
let
|
|
19
|
-
return e.answerSnapshot =
|
|
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
|
|
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 {
|
|
35
|
+
export { i as resolveAnswerPartialDelta, a as resolveThinkingDelta, r as rotateAnswerSegment };
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
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
|
|
4
|
-
import { stat as
|
|
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
|
-
|
|
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
|
|
23
|
+
function s(e) {
|
|
21
24
|
return !!e.errorInfo;
|
|
22
25
|
}
|
|
23
|
-
async function
|
|
24
|
-
let
|
|
25
|
-
return await Promise.all(
|
|
26
|
-
let
|
|
27
|
-
if ("error" in
|
|
28
|
-
|
|
29
|
-
index:
|
|
30
|
-
fileName:
|
|
31
|
-
reason:
|
|
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
|
|
39
|
+
let e = await i(l.absolutePath);
|
|
37
40
|
if (!e.isFile()) {
|
|
38
|
-
|
|
39
|
-
index:
|
|
40
|
-
fileName:
|
|
41
|
+
s.push({
|
|
42
|
+
index: c,
|
|
43
|
+
fileName: r.basename(l.absolutePath),
|
|
41
44
|
reason: "文件不存在或不可读"
|
|
42
45
|
});
|
|
43
46
|
return;
|
|
44
47
|
}
|
|
45
|
-
|
|
46
|
-
index:
|
|
47
|
-
fileName:
|
|
48
|
+
o.push({
|
|
49
|
+
index: c,
|
|
50
|
+
fileName: r.basename(l.absolutePath),
|
|
48
51
|
fileSize: e.size,
|
|
49
|
-
absolutePath:
|
|
52
|
+
absolutePath: l.absolutePath
|
|
50
53
|
});
|
|
51
|
-
} catch {
|
|
52
|
-
a.push({
|
|
53
|
-
index:
|
|
54
|
-
fileName:
|
|
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
|
-
})),
|
|
59
|
-
ok:
|
|
60
|
-
failed:
|
|
61
|
+
})), o.sort((e, t) => e.index - t.index), {
|
|
62
|
+
ok: o,
|
|
63
|
+
failed: s
|
|
61
64
|
};
|
|
62
65
|
}
|
|
63
66
|
//#endregion
|
|
64
|
-
export {
|
|
67
|
+
export { s as isErrorItem, c as preflight, o as sleep };
|
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|