@clawos-dev/clawd 0.2.254 → 0.2.256
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/cli.cjs +230 -128
- package/dist/dispatch/mcp-server.cjs +40 -6
- package/dist/share-md-viewer-error.html +1 -1
- package/dist/share-md-viewer.html +1 -1
- package/dist/share-ui/assets/{guest-B7FHhw7e.js → guest-hgeG05IO.js} +51 -51
- package/dist/share-ui/assets/{guest-53QJqRhe.css → guest-kK-Ir45s.css} +1 -1
- package/dist/share-ui/guest.html +2 -2
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -166,6 +166,10 @@ var init_methods = __esm({
|
|
|
166
166
|
// owner UI 授权 contact 反向操作我的设备(跨设备 exec 开关 remoteAccessAllowed)。
|
|
167
167
|
// 详见 protocol/src/contact-remote-access.ts.
|
|
168
168
|
"contact:setRemoteAccess",
|
|
169
|
+
// ---- contact:handshake (双向确认建联握手, spec 2026-07-22) ----
|
|
170
|
+
// device:connect 临时连接调用;被调方同步落库+broadcast 后回 ok(携带身份载荷)。
|
|
171
|
+
// 替代旧 whoami+auto-reverse 组合;whoami 对 connect-token guest 已封(旧客户端拦截)。
|
|
172
|
+
"contact:handshake",
|
|
169
173
|
// ---- visitor:* (web 访客 · persona web 分享,spec 2026-06-24-persona-web-share-design) ----
|
|
170
174
|
// owner-only:列出登录访问过本机 public persona 的 web 访客(零安装、飞书登录、daemon 自签
|
|
171
175
|
// visitor token)。存储 ~/.clawd/visitors.json(VisitorStore),登录(exchange)即 upsert;
|
|
@@ -5367,13 +5371,13 @@ var init_schemas = __esm({
|
|
|
5367
5371
|
* guest 的设备 id / 人 id / 来源全部由 connect token 的 subDevice/subOwner/provider 签名背书
|
|
5368
5372
|
* (server 签、目标设备本地验签,不可伪造),不再接受任何自报身份。
|
|
5369
5373
|
*
|
|
5370
|
-
* selfUrl
|
|
5371
|
-
*
|
|
5374
|
+
* selfUrl 已废弃(spec 2026-07-22 双向确认建联):可达地址自报移入 contact:handshake
|
|
5375
|
+
* args——auth 层回归纯验票。旧客户端发来的 selfUrl 被 strip(zod 默认),无害;
|
|
5376
|
+
* 旧客户端本身由 whoami 的 connect-token 拒绝路径拦截(CLIENT_OUTDATED)。
|
|
5372
5377
|
*/
|
|
5373
|
-
selfUrl: external_exports.string().min(1).optional(),
|
|
5374
5378
|
/**
|
|
5375
5379
|
* 客户端自报的 user-agent 性质标签(诊断/展示用)。
|
|
5376
|
-
*
|
|
5380
|
+
* 「自报事实」,非身份(决策 #16 边界):不参与鉴权、不参与路由、
|
|
5377
5381
|
* 任何 handler 不得据此做权限判断。name 约定命名('clawd-desktop' / 'clawd-web' /
|
|
5378
5382
|
* 'clawd-mobile-android' / ...),宽容未知值(将来加新客户端不动 schema)。
|
|
5379
5383
|
* instanceId:客户端首启生成 UUID 并本地持久化——区分同种类多实例(多手机/多 desktop 同连)。
|
|
@@ -5827,6 +5831,32 @@ var init_contact = __esm({
|
|
|
5827
5831
|
}
|
|
5828
5832
|
});
|
|
5829
5833
|
|
|
5834
|
+
// ../protocol/src/contact-handshake.ts
|
|
5835
|
+
var ContactHandshakeArgsSchema, ContactHandshakeOkSchema;
|
|
5836
|
+
var init_contact_handshake = __esm({
|
|
5837
|
+
"../protocol/src/contact-handshake.ts"() {
|
|
5838
|
+
"use strict";
|
|
5839
|
+
init_zod();
|
|
5840
|
+
init_capability();
|
|
5841
|
+
ContactHandshakeArgsSchema = external_exports.object({
|
|
5842
|
+
selfUrl: external_exports.string().min(1)
|
|
5843
|
+
});
|
|
5844
|
+
ContactHandshakeOkSchema = external_exports.object({
|
|
5845
|
+
type: external_exports.literal("contact:handshake:ok"),
|
|
5846
|
+
owner: external_exports.object({
|
|
5847
|
+
/** 被调方 deviceId(路由主键;调用方落 Contact 的主键用自己请求的 args.deviceId,此值供校验/展示) */
|
|
5848
|
+
id: external_exports.string(),
|
|
5849
|
+
/** 归属人(落 Contact.ownerId / provider,显示分组用) */
|
|
5850
|
+
ownerId: external_exports.string(),
|
|
5851
|
+
provider: external_exports.string(),
|
|
5852
|
+
displayName: external_exports.string()
|
|
5853
|
+
}),
|
|
5854
|
+
/** 被调方授调用方的 grants(落 Contact.grants) */
|
|
5855
|
+
grants: external_exports.array(GrantSchema)
|
|
5856
|
+
});
|
|
5857
|
+
}
|
|
5858
|
+
});
|
|
5859
|
+
|
|
5830
5860
|
// ../protocol/src/contact-remote-access.ts
|
|
5831
5861
|
var ContactSetRemoteAccessArgsSchema, ContactSetRemoteAccessOkSchema, ContactRemoteAccessUpdatedFrameSchema;
|
|
5832
5862
|
var init_contact_remote_access = __esm({
|
|
@@ -6208,7 +6238,13 @@ var init_dispatch = __esm({
|
|
|
6208
6238
|
// 合法值集与 claude adapter capabilities.models[].id 一致(见 daemon
|
|
6209
6239
|
// tools/claude.ts CLAUDE_MODEL_IDS);handler 层做二次校验,非法值 fail-fast。
|
|
6210
6240
|
// protocol 层保持 string 便于未来跨 tool 扩展(届时 union 收合法字面量)。
|
|
6211
|
-
model: external_exports.string().min(1).optional()
|
|
6241
|
+
model: external_exports.string().min(1).optional(),
|
|
6242
|
+
// 可选:给新 worker session 起 label(写入 SessionFile.label,sidebar 显示)。
|
|
6243
|
+
// 仅在**新开 dispatch**(dispatchId 缺席)时生效;续派(dispatchId 非空)
|
|
6244
|
+
// 时 daemon 层静默丢弃(避免半途改 label 造成 sidebar 抖动)。
|
|
6245
|
+
// 上限 60 字符——sidebar 显示 tolerance,卡在 dispatch 入口即可。
|
|
6246
|
+
// 协议层不硬拒 sessionLabel + dispatchId 同传,保留 forward-compat。
|
|
6247
|
+
sessionLabel: external_exports.string().min(1).max(60).optional()
|
|
6212
6248
|
}).refine((v2) => v2.targetPersona !== void 0 || v2.dispatchId !== void 0, {
|
|
6213
6249
|
message: "either targetPersona (new dispatch) or dispatchId (follow-up) is required"
|
|
6214
6250
|
}).refine((v2) => !(v2.dispatchId !== void 0 && v2.targetDeviceId !== void 0), {
|
|
@@ -6315,6 +6351,7 @@ var init_runtime = __esm({
|
|
|
6315
6351
|
init_capability();
|
|
6316
6352
|
init_inbox();
|
|
6317
6353
|
init_contact();
|
|
6354
|
+
init_contact_handshake();
|
|
6318
6355
|
init_contact_remote_access();
|
|
6319
6356
|
init_peer_exec();
|
|
6320
6357
|
init_extension();
|
|
@@ -41901,20 +41938,23 @@ var ATTACHMENT_SHARING_HINT = `## \u628A\u4EA7\u51FA\u6587\u4EF6\u5206\u4EAB\u7E
|
|
|
41901
41938
|
- \u53EA\u80FD\u5206\u4EAB\u4F60\u81EA\u5DF1\u5DE5\u4F5C\u76EE\u5F55\u5185\u7684\u6587\u4EF6\uFF0C\u8D8A\u754C\u4F1A\u88AB\u62D2\u3002
|
|
41902
41939
|
- \u88AB dispatch \u63A5\u5230\u4EFB\u52A1\u3001\u8981\u56DE\u4F20\u4EA7\u7269\u65F6\uFF1A\u628A\u7B7E\u540D URL \u5199\u8FDB \`personaDispatchComplete\` tool\uFF08\`clawd-dispatch\` MCP\uFF09\u7684 \`text\` \u91CC\uFF08\`filePaths\` \u53EA\u662F\u4F60\u672C\u673A\u7684\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u8DE8\u8BBE\u5907\u5BF9\u65B9\u6253\u4E0D\u5F00\uFF0CURL \u624D\u80FD\u6253\u5F00\uFF09\u3002`;
|
|
41903
41940
|
|
|
41941
|
+
// src/persona/no-ask-user-question-prompt.ts
|
|
41942
|
+
var NO_ASK_USER_QUESTION_HINT = `## \u9700\u8981\u8001\u677F\u62CD\u677F\u65F6\u600E\u4E48\u95EE
|
|
41943
|
+
|
|
41944
|
+
\u8981\u8001\u677F\u9009\u62E9 / \u786E\u8BA4 / \u62CD\u677F\u65F6\uFF0C\u76F4\u63A5\u6B63\u6587\u5199\u95EE\u9898 + \u5217 A/B/C \u9009\u9879\u3002\u4E0D\u8981\u8C03 AskUserQuestion tool\u3002`;
|
|
41945
|
+
|
|
41904
41946
|
// src/dispatch/system-prompt.ts
|
|
41905
41947
|
var DISPATCH_SYSTEM_PROMPT_HINT = `## \u59D4\u6D3E\u7ED9\u53E6\u4E00\u4E2A persona\uFF08dispatch\uFF09
|
|
41906
41948
|
|
|
41907
|
-
\u5F53\u7528\u6237\u6D88\u606F\u91CC\u542B \`@persona/<id>\`\uFF08\u5982 \`@persona/persona-app-builder\`\uFF09\u65F6\uFF0C**\u4F60\u5FC5\u987B**\u7ACB\u5373\u8C03 \`clawd-dispatch\` MCP
|
|
41949
|
+
\u5F53\u7528\u6237\u6D88\u606F\u91CC\u542B \`@persona/<id>\`\uFF08\u5982 \`@persona/persona-app-builder\`\uFF09\u65F6\uFF0C**\u4F60\u5FC5\u987B**\u7ACB\u5373\u8C03 \`clawd-dispatch\` MCP \u7684 \`personaDispatch\` tool \u59D4\u6D3E\uFF1A
|
|
41908
41950
|
|
|
41909
41951
|
- \`targetPersona = <id>\`\uFF08@ token \u91CC\u7684 persona id\uFF09
|
|
41910
41952
|
- \`prompt = \u53BB\u6389 @persona/<id> token \u7684\u7528\u6237\u539F\u6587\`
|
|
41911
41953
|
|
|
41912
|
-
|
|
41954
|
+
\u544A\u8BC9\u7528\u6237"\u4EFB\u52A1\u5DF2\u4EA4\u529E"\u5373\u53EF\u7ED3\u675F\u672C\u8F6E\u3002\u7ED3\u679C\u4F1A\u4F5C\u4E3A\u4E00\u6761\u65B0\u6D88\u606F\u81EA\u52A8\u6CE8\u5165\u56DE\u4F60\u7684 session\uFF0C\u5C4A\u65F6\u518D\u7EFC\u5408\u8F6C\u8FBE\u7ED9\u7528\u6237\u3002\u60F3\u67E5\u8FDB\u5C55\u65F6\uFF08\u7528\u6237\u95EE\u8D77\uFF09\u8C03\u540C\u4E00 MCP \u7684 \`personaDispatchList\`\u3002
|
|
41913
41955
|
|
|
41914
|
-
- **\u4E0D\u8981**\u5C1D\u8BD5\u81EA\u5DF1\u6267\u884C\uFF08\u4F60\u4E0D\u662F\u90A3\u4E2A persona\uFF09
|
|
41915
|
-
- **\u4E0D\u8981** in-place \u6A21\u4EFF\u5BF9\u65B9\u4EBA\u683C \u2014\u2014 dispatch \u662F\u771F\u5207\u5230\u53E6\u4E00\u4E2A\u72EC\u7ACB session
|
|
41956
|
+
- **\u4E0D\u8981**\u5C1D\u8BD5\u81EA\u5DF1\u6267\u884C\uFF08\u4F60\u4E0D\u662F\u90A3\u4E2A persona\uFF09\uFF0C\u4E5F\u4E0D\u8981 in-place \u6A21\u4EFF\u5BF9\u65B9\u4EBA\u683C \u2014\u2014 dispatch \u662F\u771F\u5207\u5230\u53E6\u4E00\u4E2A\u72EC\u7ACB session
|
|
41916
41957
|
- \u4E00\u53E5\u8BDD\u91CC\u591A\u4E2A \`@persona/<id>\` \u65F6\u9009\u7B2C\u4E00\u4E2A\uFF08tool \u4E00\u6B21\u53EA\u5904\u7406\u4E00\u4E2A target\uFF09
|
|
41917
|
-
- \u59D4\u6D3E\u5931\u8D25\uFF08tool_result.is_error=true\uFF09\u65F6\u5982\u5B9E\u544A\u8BC9\u7528\u6237\u539F\u56E0\uFF0C**\u4E0D\u8981\u91CD\u8BD5**
|
|
41918
41958
|
|
|
41919
41959
|
### \u7EED\u6D3E\uFF08\u540C\u4E00\u4EF6\u4E8B\u7684\u540E\u7EED\u6307\u4EE4\uFF09
|
|
41920
41960
|
|
|
@@ -41922,15 +41962,15 @@ tool \u4F1A**\u7ACB\u5373\u8FD4\u56DE** dispatchId\uFF08\u5F02\u6B65\u59D4\u6D3E
|
|
|
41922
41962
|
|
|
41923
41963
|
### \u8DE8\u8BBE\u5907\u59D4\u6D3E\uFF08remote persona\uFF09
|
|
41924
41964
|
|
|
41925
|
-
\u5F53 @ token \u5F62\u5982 \`@persona/<id>@<deviceId>\`\uFF08\u7B2C\u4E8C\u6BB5\u662F\u8054\u7CFB\u4EBA**\u8BBE\u5907\u7684 deviceId**\uFF0CUI \u9009\u8054\u7CFB\u4EBA\u65F6\u5DF2\u89E3\u6790\u597D\u586B\u5165\uFF09\u65F6\uFF0C\u662F\u628A\u4EFB\u52A1\u59D4\u6D3E\u7ED9\u90A3\u53F0\u8BBE\u5907\u4E0A\u7684 persona\u3002\u9664 targetPersona / prompt \u5916\u518D\u5E26\
|
|
41965
|
+
\u5F53 @ token \u5F62\u5982 \`@persona/<id>@<deviceId>\`\uFF08\u7B2C\u4E8C\u6BB5\u662F\u8054\u7CFB\u4EBA**\u8BBE\u5907\u7684 deviceId**\uFF0CUI \u9009\u8054\u7CFB\u4EBA\u65F6\u5DF2\u89E3\u6790\u597D\u586B\u5165\uFF09\u65F6\uFF0C\u662F\u628A\u4EFB\u52A1\u59D4\u6D3E\u7ED9\u90A3\u53F0\u8BBE\u5907\u4E0A\u7684 persona\u3002\u9664 targetPersona / prompt \u5916\u518D\u5E26\uFF1A
|
|
41926
41966
|
|
|
41927
41967
|
- \`targetDeviceId = <token \u7B2C\u4E8C\u6BB5\u7684 deviceId\uFF0C\u539F\u6837\u586B>\`
|
|
41928
41968
|
|
|
41929
|
-
deviceId \u76F4\u63A5\u53D6\u81EA token\uFF0C\u4E0D\u8981\u6539\u5199\u6216\u731C\u6D4B\u3002
|
|
41969
|
+
deviceId \u76F4\u63A5\u53D6\u81EA token\uFF0C\u4E0D\u8981\u6539\u5199\u6216\u731C\u6D4B\u3002
|
|
41930
41970
|
|
|
41931
41971
|
## \u88AB dispatch \u63A5\u5230\u4EFB\u52A1
|
|
41932
41972
|
|
|
41933
|
-
\u5982\u679C\u4F60\u7684 session \u7B2C\u4E00\u6761 user message \u662F\u4EE5 \`[Dispatched from owner \u2014 another persona session\u59D4\u6D3E\u7ED9\u4F60\u6267\u884C\u4EFB\u52A1]\` \u8D77\u5934\u7684 plain text \u4EFB\u52A1\u5305\uFF0C\u4F60\u662F\u88AB\u53E6\u4E00\u4E2A persona \u59D4\u6D3E\u6765\u5E72\u6D3B\u7684\
|
|
41973
|
+
\u5982\u679C\u4F60\u7684 session \u7B2C\u4E00\u6761 user message \u662F\u4EE5 \`[Dispatched from owner \u2014 another persona session\u59D4\u6D3E\u7ED9\u4F60\u6267\u884C\u4EFB\u52A1]\` \u8D77\u5934\u7684 plain text \u4EFB\u52A1\u5305\uFF0C\u4F60\u662F\u88AB\u53E6\u4E00\u4E2A persona \u59D4\u6D3E\u6765\u5E72\u6D3B\u7684\uFF1A
|
|
41934
41974
|
|
|
41935
41975
|
- \u4EFB\u52A1\u5728 \`Task:\` \u6BB5
|
|
41936
41976
|
- \u539F\u4F1A\u8BDD\u7684\u5BF9\u8BDD\u5386\u53F2\u5728 \`Source conversation\` \u6BB5\u7ED9\u7684 jsonl \u8DEF\u5F84\u4E0B\uFF1B\u6309\u9700 Read \u5B83\u4E86\u89E3\u4E0A\u4E0B\u6587\uFF08\u4E0D\u5FC5\u8BFB\u5B8C\uFF09
|
|
@@ -42095,6 +42135,9 @@ function buildSpawnContext(state, deps) {
|
|
|
42095
42135
|
if (daemonUrl) {
|
|
42096
42136
|
ctx.extraSystemPrompt = (ctx.extraSystemPrompt ? ctx.extraSystemPrompt + "\n\n" : "") + ATTACHMENT_SHARING_HINT;
|
|
42097
42137
|
}
|
|
42138
|
+
if (meta?.personaMode && (file.tool ?? "claude") === "claude") {
|
|
42139
|
+
ctx.extraSystemPrompt = (ctx.extraSystemPrompt ? ctx.extraSystemPrompt + "\n\n" : "") + NO_ASK_USER_QUESTION_HINT;
|
|
42140
|
+
}
|
|
42098
42141
|
if (meta?.extraSettings) {
|
|
42099
42142
|
ctx.extraSettings = meta.extraSettings;
|
|
42100
42143
|
}
|
|
@@ -44727,6 +44770,8 @@ var SessionManager = class {
|
|
|
44727
44770
|
} : {},
|
|
44728
44771
|
// T-20:dispatch model override 落盘(reducer spawn 路径读 SessionFile.model 生成 --model flag)
|
|
44729
44772
|
...args.model ? { model: args.model } : {},
|
|
44773
|
+
// butler 顶部入口:dispatch 传下来的 label 落盘,sidebar 显示(否则兜底 cwd-basename)
|
|
44774
|
+
...args.label ? { label: args.label } : {},
|
|
44730
44775
|
createdAt: now,
|
|
44731
44776
|
updatedAt: now
|
|
44732
44777
|
};
|
|
@@ -49384,7 +49429,10 @@ function buildPersonaDispatchHandlers(deps) {
|
|
|
49384
49429
|
guestDisplayName: ctx.principal.displayName,
|
|
49385
49430
|
// T-31: 把 A 的飞书 union_id 透传给 B(connectGuestContext 已把它挂在 principal.feishuUnionId)
|
|
49386
49431
|
...ctx.principal.feishuUnionId ? { guestFeishuUnionId: ctx.principal.feishuUnionId } : {},
|
|
49387
|
-
...args.model ? { model: args.model } : {}
|
|
49432
|
+
...args.model ? { model: args.model } : {},
|
|
49433
|
+
// butler 顶部入口:peer daemon 若在 forwardToPeer 里传了 sessionLabel(当前
|
|
49434
|
+
// 未接线,作 forward-compat 预留),这里也透给 spawnB
|
|
49435
|
+
...args.sessionLabel ? { label: args.sessionLabel } : {}
|
|
49388
49436
|
}).then(() => logger?.info("dispatch.spawnB.ok", { dispatchId: dispatchId2 })).catch((err) => {
|
|
49389
49437
|
const reason = err instanceof Error ? err.message : String(err);
|
|
49390
49438
|
logger?.warn("dispatch.spawnB.failed", { dispatchId: dispatchId2, reason });
|
|
@@ -49419,7 +49467,9 @@ function buildPersonaDispatchHandlers(deps) {
|
|
|
49419
49467
|
sourceSessionId,
|
|
49420
49468
|
targetPersona,
|
|
49421
49469
|
prompt: args.prompt,
|
|
49422
|
-
...args.model ? { model: args.model } : {}
|
|
49470
|
+
...args.model ? { model: args.model } : {},
|
|
49471
|
+
// butler 顶部入口(spec 2026-07-27):sessionLabel 落到 SessionFile.label 让 sidebar 显示
|
|
49472
|
+
...args.sessionLabel ? { label: args.sessionLabel } : {}
|
|
49423
49473
|
}).then(() => {
|
|
49424
49474
|
logger?.info("dispatch.spawnB.ok", { dispatchId });
|
|
49425
49475
|
}).catch((err) => {
|
|
@@ -51058,10 +51108,7 @@ var AuthGate = class {
|
|
|
51058
51108
|
st.pending = true;
|
|
51059
51109
|
let r;
|
|
51060
51110
|
try {
|
|
51061
|
-
r = await this.opts.authenticate(
|
|
51062
|
-
parsed.data.token,
|
|
51063
|
-
parsed.data.selfUrl
|
|
51064
|
-
);
|
|
51111
|
+
r = await this.opts.authenticate(parsed.data.token);
|
|
51065
51112
|
} finally {
|
|
51066
51113
|
st.pending = false;
|
|
51067
51114
|
}
|
|
@@ -51165,7 +51212,8 @@ function constantTimeEqual2(a, b2) {
|
|
|
51165
51212
|
function ownerContext(ownerPrincipalId, displayName, feishuUnionId) {
|
|
51166
51213
|
return {
|
|
51167
51214
|
principal: makeOwnerPrincipal(ownerPrincipalId, displayName, feishuUnionId),
|
|
51168
|
-
grants: [{ resource: { type: "*" }, actions: ["admin"] }]
|
|
51215
|
+
grants: [{ resource: { type: "*" }, actions: ["admin"] }],
|
|
51216
|
+
tokenKind: "owner"
|
|
51169
51217
|
};
|
|
51170
51218
|
}
|
|
51171
51219
|
function connectGuestContext(subDevice, subOwner, provider, displayName) {
|
|
@@ -51175,6 +51223,7 @@ function connectGuestContext(subDevice, subOwner, provider, displayName) {
|
|
|
51175
51223
|
resource: { ...g2.resource },
|
|
51176
51224
|
actions: [...g2.actions]
|
|
51177
51225
|
})),
|
|
51226
|
+
tokenKind: "connect",
|
|
51178
51227
|
ownerId: subOwner,
|
|
51179
51228
|
provider
|
|
51180
51229
|
};
|
|
@@ -51186,6 +51235,7 @@ function visitorGuestContext(p2) {
|
|
|
51186
51235
|
resource: { ...g2.resource },
|
|
51187
51236
|
actions: [...g2.actions]
|
|
51188
51237
|
})),
|
|
51238
|
+
tokenKind: "visitor",
|
|
51189
51239
|
ownerId: p2.visitorId,
|
|
51190
51240
|
provider: p2.provider
|
|
51191
51241
|
};
|
|
@@ -51534,6 +51584,7 @@ var ContactStore = class {
|
|
|
51534
51584
|
|
|
51535
51585
|
// src/contact/connect-remote.ts
|
|
51536
51586
|
var crypto5 = __toESM(require("crypto"), 1);
|
|
51587
|
+
init_protocol();
|
|
51537
51588
|
var HANDSHAKE_TIMEOUT_MS = 5e3;
|
|
51538
51589
|
var RPC_TIMEOUT_MS = 5e3;
|
|
51539
51590
|
async function connectRemote(args) {
|
|
@@ -51546,17 +51597,7 @@ async function connectRemote(args) {
|
|
|
51546
51597
|
});
|
|
51547
51598
|
ws.once("error", onError);
|
|
51548
51599
|
});
|
|
51549
|
-
ws.send(
|
|
51550
|
-
JSON.stringify({
|
|
51551
|
-
type: "auth",
|
|
51552
|
-
token: args.token,
|
|
51553
|
-
// 自动反向(spec 决策 #11):带本机可达地址时对方反向建联系人;
|
|
51554
|
-
// 缺省时省略字段(不发空串),对方 schema selfUrl 是 .min(1).optional()。
|
|
51555
|
-
// 决策 #16:身份字段(selfPrincipalId/selfDisplayName/selfDeviceId)已删——
|
|
51556
|
-
// 对方从 connect token 签名背书取 deviceId/ownerId/provider,不信自报。
|
|
51557
|
-
...args.selfUrl ? { selfUrl: args.selfUrl } : {}
|
|
51558
|
-
})
|
|
51559
|
-
);
|
|
51600
|
+
ws.send(JSON.stringify({ type: "auth", token: args.token }));
|
|
51560
51601
|
await new Promise((resolve6, reject) => {
|
|
51561
51602
|
const onMessage = (raw) => {
|
|
51562
51603
|
let f;
|
|
@@ -51608,7 +51649,7 @@ async function connectRemote(args) {
|
|
|
51608
51649
|
}
|
|
51609
51650
|
if (f.requestId !== requestId) return;
|
|
51610
51651
|
ws.off("message", onMessage);
|
|
51611
|
-
if (f.error || f.type?.endsWith(":error")) {
|
|
51652
|
+
if (f.error || f.type === "error" || f.type?.endsWith(":error")) {
|
|
51612
51653
|
reject(new Error(f.message ?? f.error ?? "rpc error"));
|
|
51613
51654
|
} else {
|
|
51614
51655
|
resolve6(f);
|
|
@@ -51623,13 +51664,14 @@ async function connectRemote(args) {
|
|
|
51623
51664
|
});
|
|
51624
51665
|
}
|
|
51625
51666
|
return {
|
|
51626
|
-
|
|
51627
|
-
const
|
|
51667
|
+
handshake: async (selfUrl) => {
|
|
51668
|
+
const raw = await call("contact:handshake", { selfUrl });
|
|
51669
|
+
const f = ContactHandshakeOkSchema.parse(raw);
|
|
51628
51670
|
return {
|
|
51629
51671
|
ownerId: f.owner.ownerId,
|
|
51630
51672
|
provider: f.owner.provider,
|
|
51631
51673
|
displayName: f.owner.displayName,
|
|
51632
|
-
grants: f.
|
|
51674
|
+
grants: f.grants
|
|
51633
51675
|
};
|
|
51634
51676
|
},
|
|
51635
51677
|
close: () => {
|
|
@@ -51641,43 +51683,6 @@ async function connectRemote(args) {
|
|
|
51641
51683
|
};
|
|
51642
51684
|
}
|
|
51643
51685
|
|
|
51644
|
-
// src/contact/auto-reverse.ts
|
|
51645
|
-
init_protocol();
|
|
51646
|
-
async function autoReverseContact(args) {
|
|
51647
|
-
if (!args.selfUrl) return;
|
|
51648
|
-
const now = args.now ?? Date.now;
|
|
51649
|
-
const grants = PERSONAL_CAP_GRANTS.map((g2) => ({
|
|
51650
|
-
resource: { ...g2.resource },
|
|
51651
|
-
actions: [...g2.actions]
|
|
51652
|
-
}));
|
|
51653
|
-
const existing = args.store.get(args.deviceId) ?? void 0;
|
|
51654
|
-
const base = {
|
|
51655
|
-
deviceId: args.deviceId,
|
|
51656
|
-
ownerId: args.ownerId,
|
|
51657
|
-
provider: args.provider,
|
|
51658
|
-
displayName: args.displayName,
|
|
51659
|
-
remoteUrl: args.selfUrl,
|
|
51660
|
-
connectToken: "",
|
|
51661
|
-
grants,
|
|
51662
|
-
addedAt: now(),
|
|
51663
|
-
pinnedAt: existing?.pinnedAt ?? null,
|
|
51664
|
-
remoteAccessAllowed: existing?.remoteAccessAllowed ?? false
|
|
51665
|
-
};
|
|
51666
|
-
args.store.upsert(base);
|
|
51667
|
-
args.broadcast({ type: "contact:added", contact: base });
|
|
51668
|
-
if (!args.exchangeToken) return;
|
|
51669
|
-
let token;
|
|
51670
|
-
try {
|
|
51671
|
-
token = await args.exchangeToken(args.deviceId);
|
|
51672
|
-
} catch {
|
|
51673
|
-
return;
|
|
51674
|
-
}
|
|
51675
|
-
if (!token) return;
|
|
51676
|
-
const upgraded = { ...base, connectToken: token };
|
|
51677
|
-
args.store.upsert(upgraded);
|
|
51678
|
-
args.broadcast({ type: "contact:added", contact: upgraded });
|
|
51679
|
-
}
|
|
51680
|
-
|
|
51681
51686
|
// src/migrations/2026-05-20-flatten-sessions.ts
|
|
51682
51687
|
var fs36 = __toESM(require("fs"), 1);
|
|
51683
51688
|
var path37 = __toESM(require("path"), 1);
|
|
@@ -55283,6 +55288,45 @@ function buildInboxHandlers(deps) {
|
|
|
55283
55288
|
|
|
55284
55289
|
// src/handlers/contact.ts
|
|
55285
55290
|
init_protocol();
|
|
55291
|
+
|
|
55292
|
+
// src/contact/auto-reverse.ts
|
|
55293
|
+
init_protocol();
|
|
55294
|
+
function upsertPeerContact(args) {
|
|
55295
|
+
const now = args.now ?? Date.now;
|
|
55296
|
+
const grants = PERSONAL_CAP_GRANTS.map((g2) => ({
|
|
55297
|
+
resource: { ...g2.resource },
|
|
55298
|
+
actions: [...g2.actions]
|
|
55299
|
+
}));
|
|
55300
|
+
const existing = args.store.get(args.deviceId) ?? void 0;
|
|
55301
|
+
const base = {
|
|
55302
|
+
deviceId: args.deviceId,
|
|
55303
|
+
ownerId: args.ownerId,
|
|
55304
|
+
provider: args.provider,
|
|
55305
|
+
displayName: args.displayName,
|
|
55306
|
+
remoteUrl: args.selfUrl,
|
|
55307
|
+
connectToken: "",
|
|
55308
|
+
grants,
|
|
55309
|
+
addedAt: now(),
|
|
55310
|
+
pinnedAt: existing?.pinnedAt ?? null,
|
|
55311
|
+
remoteAccessAllowed: existing?.remoteAccessAllowed ?? false
|
|
55312
|
+
};
|
|
55313
|
+
args.store.upsert(base);
|
|
55314
|
+
args.broadcast({ type: "contact:added", contact: base });
|
|
55315
|
+
return base;
|
|
55316
|
+
}
|
|
55317
|
+
async function backfillConnectToken(args) {
|
|
55318
|
+
try {
|
|
55319
|
+
const token = await args.exchangeToken(args.contact.deviceId);
|
|
55320
|
+
if (!token) return;
|
|
55321
|
+
const upgraded = { ...args.contact, connectToken: token };
|
|
55322
|
+
args.store.upsert(upgraded);
|
|
55323
|
+
args.broadcast({ type: "contact:added", contact: upgraded });
|
|
55324
|
+
} catch {
|
|
55325
|
+
return;
|
|
55326
|
+
}
|
|
55327
|
+
}
|
|
55328
|
+
|
|
55329
|
+
// src/handlers/contact.ts
|
|
55286
55330
|
function ensureOwner(ctx) {
|
|
55287
55331
|
if (!ctx || ctx.principal.kind !== "owner") {
|
|
55288
55332
|
throw new ClawdError(
|
|
@@ -55342,10 +55386,49 @@ function buildContactHandlers(deps) {
|
|
|
55342
55386
|
}
|
|
55343
55387
|
};
|
|
55344
55388
|
};
|
|
55389
|
+
const handshake = async (frame, _client, ctx) => {
|
|
55390
|
+
if (!ctx || ctx.tokenKind !== "connect") {
|
|
55391
|
+
throw new ClawdError(
|
|
55392
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
55393
|
+
"contact:handshake \u4EC5\u63A5\u53D7 connect-token \u8FDE\u63A5\uFF08\u8BBE\u5907\u4E92\u8FDE\u63E1\u624B\u4E13\u7528\uFF09"
|
|
55394
|
+
);
|
|
55395
|
+
}
|
|
55396
|
+
const { type: _t, requestId: _r, ...rest } = frame;
|
|
55397
|
+
const args = ContactHandshakeArgsSchema.parse(rest);
|
|
55398
|
+
const contact = upsertPeerContact({
|
|
55399
|
+
store: deps.store,
|
|
55400
|
+
broadcast: deps.broadcast,
|
|
55401
|
+
deviceId: ctx.principal.id,
|
|
55402
|
+
ownerId: ctx.ownerId ?? "",
|
|
55403
|
+
provider: ctx.provider ?? "",
|
|
55404
|
+
displayName: ctx.principal.displayName ?? ctx.principal.id,
|
|
55405
|
+
selfUrl: args.selfUrl,
|
|
55406
|
+
now: deps.now
|
|
55407
|
+
});
|
|
55408
|
+
void backfillConnectToken({
|
|
55409
|
+
store: deps.store,
|
|
55410
|
+
broadcast: deps.broadcast,
|
|
55411
|
+
contact,
|
|
55412
|
+
exchangeToken: deps.exchangeToken
|
|
55413
|
+
});
|
|
55414
|
+
return {
|
|
55415
|
+
response: {
|
|
55416
|
+
type: "contact:handshake:ok",
|
|
55417
|
+
owner: {
|
|
55418
|
+
id: deps.ownerPrincipalId,
|
|
55419
|
+
ownerId: deps.ownerId,
|
|
55420
|
+
provider: deps.ownerProvider,
|
|
55421
|
+
displayName: deps.ownerDisplayName
|
|
55422
|
+
},
|
|
55423
|
+
grants: ctx.grants
|
|
55424
|
+
}
|
|
55425
|
+
};
|
|
55426
|
+
};
|
|
55345
55427
|
return {
|
|
55346
55428
|
"contact:list": list,
|
|
55347
55429
|
"contact:pin": pin,
|
|
55348
|
-
"contact:remove": remove
|
|
55430
|
+
"contact:remove": remove,
|
|
55431
|
+
"contact:handshake": handshake
|
|
55349
55432
|
};
|
|
55350
55433
|
}
|
|
55351
55434
|
|
|
@@ -55398,6 +55481,14 @@ function buildWhoamiHandler(deps) {
|
|
|
55398
55481
|
if (!ctx) {
|
|
55399
55482
|
throw new ClawdError(ERROR_CODES.INTERNAL, "whoami: missing ConnectionContext");
|
|
55400
55483
|
}
|
|
55484
|
+
if (ctx.tokenKind === "connect") {
|
|
55485
|
+
return {
|
|
55486
|
+
response: {
|
|
55487
|
+
type: "whoami:error",
|
|
55488
|
+
message: "CLIENT_OUTDATED: \u8BF7\u5347\u7EA7\u672C\u673A clawd \u540E\u91CD\u8BD5"
|
|
55489
|
+
}
|
|
55490
|
+
};
|
|
55491
|
+
}
|
|
55401
55492
|
const owner = {
|
|
55402
55493
|
...makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName),
|
|
55403
55494
|
ownerId: deps.ownerId,
|
|
@@ -55504,6 +55595,13 @@ function buildDeviceHandlers(deps) {
|
|
|
55504
55595
|
ensureOwner3(ctx);
|
|
55505
55596
|
const { type: _t, requestId: _r, ...rest } = frame;
|
|
55506
55597
|
const args = DeviceConnectArgsSchema.parse(rest);
|
|
55598
|
+
const selfUrl = deps.selfUrl();
|
|
55599
|
+
if (!selfUrl) {
|
|
55600
|
+
throw new ClawdError(
|
|
55601
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
55602
|
+
"TUNNEL_REQUIRED: \u672C\u673A\u672A\u5F00 tunnel\uFF0C\u5F00\u542F\u540E\u91CD\u8BD5"
|
|
55603
|
+
);
|
|
55604
|
+
}
|
|
55507
55605
|
const exchanged = await deps.exchange(args.deviceId);
|
|
55508
55606
|
const url = args.url ?? exchanged.deviceUrl;
|
|
55509
55607
|
if (!url) {
|
|
@@ -55514,14 +55612,7 @@ function buildDeviceHandlers(deps) {
|
|
|
55514
55612
|
}
|
|
55515
55613
|
let conn;
|
|
55516
55614
|
try {
|
|
55517
|
-
conn = await deps.connectRemote({
|
|
55518
|
-
url,
|
|
55519
|
-
token: exchanged.token,
|
|
55520
|
-
// 自动反向(spec 决策 #11):自报本机可达地址,让对方反向加我为联系人;
|
|
55521
|
-
// 本机无 tunnel → null → connectRemote 省略 selfUrl 字段(不造假数据)。
|
|
55522
|
-
// 身份不自报:对方从 token 签名背书取我的 deviceId/ownerId/provider(决策 #16)。
|
|
55523
|
-
selfUrl: deps.selfUrl() ?? void 0
|
|
55524
|
-
});
|
|
55615
|
+
conn = await deps.connectRemote({ url, token: exchanged.token });
|
|
55525
55616
|
} catch (e) {
|
|
55526
55617
|
throw new ClawdError(
|
|
55527
55618
|
ERROR_CODES.VALIDATION_ERROR,
|
|
@@ -55529,21 +55620,33 @@ function buildDeviceHandlers(deps) {
|
|
|
55529
55620
|
);
|
|
55530
55621
|
}
|
|
55531
55622
|
try {
|
|
55532
|
-
|
|
55623
|
+
let hs;
|
|
55624
|
+
try {
|
|
55625
|
+
hs = await conn.handshake(selfUrl);
|
|
55626
|
+
} catch (e) {
|
|
55627
|
+
const msg = e.message;
|
|
55628
|
+
if (/unknown method|METHOD_NOT_ALLOWED/i.test(msg)) {
|
|
55629
|
+
throw new ClawdError(
|
|
55630
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
55631
|
+
"REVERSE_UNSUPPORTED: \u5BF9\u65B9 clawd \u7248\u672C\u8FC7\u65E7\uFF0C\u8BF7\u8BA9\u5BF9\u65B9\u5347\u7EA7\u540E\u91CD\u8BD5"
|
|
55632
|
+
);
|
|
55633
|
+
}
|
|
55634
|
+
throw e;
|
|
55635
|
+
}
|
|
55533
55636
|
const existing = deps.store.get(args.deviceId) ?? void 0;
|
|
55534
55637
|
const contact = {
|
|
55535
55638
|
deviceId: args.deviceId,
|
|
55536
|
-
ownerId:
|
|
55537
|
-
provider:
|
|
55639
|
+
ownerId: hs.ownerId,
|
|
55640
|
+
provider: hs.provider,
|
|
55538
55641
|
// 显示名优先用调用方传的 name:公共目录点击连接带 clawd_devices.name(对外展示名,
|
|
55539
|
-
// 如"公共1");"输入标识"连接不带 name → 回落
|
|
55540
|
-
// 归属人身份不丢(ownerId/provider 仍来自
|
|
55541
|
-
displayName: args.name ||
|
|
55642
|
+
// 如"公共1");"输入标识"连接不带 name → 回落 handshake 报的对方 owner 真名。
|
|
55643
|
+
// 归属人身份不丢(ownerId/provider 仍来自 handshake 背书)。
|
|
55644
|
+
displayName: args.name || hs.displayName || args.deviceId,
|
|
55542
55645
|
remoteUrl: url,
|
|
55543
55646
|
// connectToken 存自包含签名 token(断线重连复用,30 天有效;
|
|
55544
55647
|
// 过期后对方拒 TOKEN_EXPIRED → 重新 device:connect 换新票)
|
|
55545
55648
|
connectToken: exchanged.token,
|
|
55546
|
-
grants:
|
|
55649
|
+
grants: hs.grants,
|
|
55547
55650
|
addedAt: now(),
|
|
55548
55651
|
pinnedAt: existing?.pinnedAt ?? null,
|
|
55549
55652
|
remoteAccessAllowed: existing?.remoteAccessAllowed ?? false
|
|
@@ -57988,7 +58091,12 @@ function buildMethodHandlers(deps) {
|
|
|
57988
58091
|
...buildContactHandlers({
|
|
57989
58092
|
store: deps.contactStore,
|
|
57990
58093
|
broadcast: deps.broadcastToOwners,
|
|
57991
|
-
now: () => Date.now()
|
|
58094
|
+
now: () => Date.now(),
|
|
58095
|
+
ownerPrincipalId: deps.ownerPrincipalId,
|
|
58096
|
+
ownerDisplayName: deps.ownerDisplayName,
|
|
58097
|
+
ownerId: deps.ownerId,
|
|
58098
|
+
ownerProvider: deps.ownerProvider,
|
|
58099
|
+
exchangeToken: deps.exchangeToken
|
|
57992
58100
|
}),
|
|
57993
58101
|
...buildContactRemoteAccessHandlers({
|
|
57994
58102
|
store: deps.contactStore,
|
|
@@ -58334,6 +58442,9 @@ var METHOD_GRANT_MAP = {
|
|
|
58334
58442
|
// contact:setRemoteAccess (owner UI 配远程访问授权):ADMIN_ANY
|
|
58335
58443
|
// (对齐 inbox:postMessage 的"能连上=有 auth,业务在 handler 校"模式)
|
|
58336
58444
|
"contact:setRemoteAccess": ADMIN_ANY,
|
|
58445
|
+
// contact:handshake: 能连上 = 已通过本地验票;handler 内校验 tokenKind==='connect'
|
|
58446
|
+
// (visitor / owner 不允许触发反向落库)。
|
|
58447
|
+
"contact:handshake": { kind: "public" },
|
|
58337
58448
|
// ---- visitor:* (访客名单,owner-only) ----
|
|
58338
58449
|
// owner 看完整访客名单(含没开会话的);guest 不可调(handler 内再 assertOwner 兜底)。
|
|
58339
58450
|
"visitor:list": ADMIN_ANY,
|
|
@@ -58986,43 +59097,10 @@ async function startDaemon(config) {
|
|
|
58986
59097
|
// Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
|
|
58987
59098
|
// owner 路径 constantTimeEqual 防侧信道;guest 路径走 verifyConnectToken (Ed25519).
|
|
58988
59099
|
expectedToken: resolvedAuthToken,
|
|
58989
|
-
|
|
58990
|
-
|
|
58991
|
-
|
|
58992
|
-
|
|
58993
|
-
store: contactStore,
|
|
58994
|
-
broadcast: (frame) => wsServer?.broadcastToOwners(frame),
|
|
58995
|
-
deviceId: result.context.principal.id,
|
|
58996
|
-
ownerId: result.context.ownerId ?? "",
|
|
58997
|
-
provider: result.context.provider ?? "",
|
|
58998
|
-
displayName: result.context.principal.displayName ?? result.context.principal.id,
|
|
58999
|
-
selfUrl,
|
|
59000
|
-
// 决策 #16 换票补全:用 owner 当前 jwt 换 aud=对方设备 的票(本机主动连对方用)。
|
|
59001
|
-
// 现读 ownerIdentityStore(热切换后用新身份);失败 → null → 保持空票。
|
|
59002
|
-
exchangeToken: async (targetDeviceId) => {
|
|
59003
|
-
const record = ownerIdentityStore.read();
|
|
59004
|
-
if (!record) return null;
|
|
59005
|
-
try {
|
|
59006
|
-
const r = await centralExchange({
|
|
59007
|
-
api: config.clawosApi ?? DEFAULT_CLAWOS_API,
|
|
59008
|
-
jwt: record.ttcToken,
|
|
59009
|
-
provider: record.identity.provider,
|
|
59010
|
-
deviceId: targetDeviceId,
|
|
59011
|
-
selfDeviceId: authFile.deviceId
|
|
59012
|
-
});
|
|
59013
|
-
return r.token;
|
|
59014
|
-
} catch (e) {
|
|
59015
|
-
logger.warn("auto-reverse exchange token failed", {
|
|
59016
|
-
targetDeviceId,
|
|
59017
|
-
err: e.message
|
|
59018
|
-
});
|
|
59019
|
-
return null;
|
|
59020
|
-
}
|
|
59021
|
-
}
|
|
59022
|
-
});
|
|
59023
|
-
}
|
|
59024
|
-
return result;
|
|
59025
|
-
},
|
|
59100
|
+
// 纯验票(spec 2026-07-22 双向确认建联):guest 入站 = 自包含 connect token
|
|
59101
|
+
// **本地验签**(Ed25519 公钥 + aud === 本机 deviceId + 未过期),零网络调用。
|
|
59102
|
+
// 反向落库不再挂在验票副作用上——落库唯一路径 = contact:handshake handler。
|
|
59103
|
+
authenticate: async (t) => authenticate(t, buildConnectAuthDeps()),
|
|
59026
59104
|
onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
|
|
59027
59105
|
buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName, ownerFeishuUnionId),
|
|
59028
59106
|
closeConnection: (h, code, reason) => wsServer?.closeClient(h.id, code, reason),
|
|
@@ -59526,8 +59604,30 @@ async function startDaemon(config) {
|
|
|
59526
59604
|
// cascade 接 store (list 统计 deletedInboxEvents).
|
|
59527
59605
|
inboxManager,
|
|
59528
59606
|
inboxStore,
|
|
59529
|
-
// 联系人列表 store(device:connect /
|
|
59607
|
+
// 联系人列表 store(device:connect 出站 / contact:handshake 入站落同一 store)
|
|
59530
59608
|
contactStore,
|
|
59609
|
+
// contact:handshake 换票回填(spec 2026-07-22):用 owner 当前 jwt 换 aud=对方设备 的票。
|
|
59610
|
+
// 现读 ownerIdentityStore(热切换后用新身份);失败 / 未登录 → null → 保持空票。
|
|
59611
|
+
exchangeToken: async (targetDeviceId) => {
|
|
59612
|
+
const record = ownerIdentityStore.read();
|
|
59613
|
+
if (!record) return null;
|
|
59614
|
+
try {
|
|
59615
|
+
const r = await centralExchange({
|
|
59616
|
+
api: config.clawosApi ?? DEFAULT_CLAWOS_API,
|
|
59617
|
+
jwt: record.ttcToken,
|
|
59618
|
+
provider: record.identity.provider,
|
|
59619
|
+
deviceId: targetDeviceId,
|
|
59620
|
+
selfDeviceId: authFile.deviceId
|
|
59621
|
+
});
|
|
59622
|
+
return r.token;
|
|
59623
|
+
} catch (e) {
|
|
59624
|
+
logger.warn("contact:handshake exchange token failed", {
|
|
59625
|
+
targetDeviceId,
|
|
59626
|
+
err: e.message
|
|
59627
|
+
});
|
|
59628
|
+
return null;
|
|
59629
|
+
}
|
|
59630
|
+
},
|
|
59531
59631
|
// inbox:sendDm 用:sessionId → session 出身(复用 attachment 同款 findOwnedSessionScope)
|
|
59532
59632
|
getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
|
|
59533
59633
|
// contact:removed broadcast;复用 capability:tokenIssued 同款通路
|
|
@@ -59692,7 +59792,9 @@ async function startDaemon(config) {
|
|
|
59692
59792
|
// T-31: 把 A 的飞书 union_id stamp 到 B SessionFile,供 cc prompt 身份卡 union_id 行
|
|
59693
59793
|
guestFeishuUnionId: args.guestFeishuUnionId,
|
|
59694
59794
|
// T-20: 可选 model override 透到 SessionFile.model
|
|
59695
|
-
...args.model ? { model: args.model } : {}
|
|
59795
|
+
...args.model ? { model: args.model } : {},
|
|
59796
|
+
// butler 顶部入口(spec 2026-07-27):可选 label 透到 SessionFile.label(sidebar 显示)
|
|
59797
|
+
...args.label ? { label: args.label } : {}
|
|
59696
59798
|
});
|
|
59697
59799
|
},
|
|
59698
59800
|
// A 角色:从 ContactStore 取 peer 可达 URL + connect token,转发到对端 daemon /rpc。
|