@clawos-dev/clawd 0.2.424 → 0.2.426
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
CHANGED
|
@@ -5974,7 +5974,7 @@ var init_contact_remote_access = __esm({
|
|
|
5974
5974
|
});
|
|
5975
5975
|
|
|
5976
5976
|
// ../protocol/src/contact.ts
|
|
5977
|
-
var ContactSchema, ContactWireSchema, ContactRemoveArgsSchema, ContactRemoveOkSchema, ContactListOkSchema, ContactAddedFrameSchema, ContactRemovedFrameSchema, ContactPinArgsSchema, ContactPinOkSchema, ContactPinnedFrameSchema;
|
|
5977
|
+
var ContactSchema, ContactWireSchema, ContactRemoveArgsSchema, ContactRemoveOkSchema, ContactListOkSchema, ContactAddedFrameSchema, ContactRemovedFrameSchema, ContactPinArgsSchema, ContactSetNoteArgsSchema, ContactSetNoteOkSchema, ContactPinOkSchema, ContactPinnedFrameSchema;
|
|
5978
5978
|
var init_contact = __esm({
|
|
5979
5979
|
"../protocol/src/contact.ts"() {
|
|
5980
5980
|
"use strict";
|
|
@@ -6015,7 +6015,17 @@ var init_contact = __esm({
|
|
|
6015
6015
|
* 跨设备 exec 的总开关(peerExec:run handler 运行时判它)。default=false,owner 显式开。
|
|
6016
6016
|
* 授权粒度 per-people (per-contact):借用别人 persona 本来就带信任对方本人的前提。
|
|
6017
6017
|
*/
|
|
6018
|
-
remoteAccessAllowed: external_exports.boolean().default(false)
|
|
6018
|
+
remoteAccessAllowed: external_exports.boolean().default(false),
|
|
6019
|
+
/**
|
|
6020
|
+
* owner 给这台设备写的备注(2026-08-31)。**我对他那台机器的私记**,不上行、不同步给对方。
|
|
6021
|
+
*
|
|
6022
|
+
* 动机:一人多设备时卡片里只能显示 deviceId(`owner-8bf96a56-6b7e-…` 这种 uuid),
|
|
6023
|
+
* 认不出哪台是哪台。备注让用户能写「老王的 MacBook」。
|
|
6024
|
+
*
|
|
6025
|
+
* 空串 = 没写。default('') 兼容老 contacts.json(无破坏性升级,不 orphan)。
|
|
6026
|
+
* 跟 pinnedAt / remoteAccessAllowed 同类:owner 已配置状态,重复建联 upsert 必须保留。
|
|
6027
|
+
*/
|
|
6028
|
+
note: external_exports.string().default("")
|
|
6019
6029
|
}).strict();
|
|
6020
6030
|
ContactWireSchema = ContactSchema;
|
|
6021
6031
|
ContactRemoveArgsSchema = external_exports.object({
|
|
@@ -6041,6 +6051,16 @@ var init_contact = __esm({
|
|
|
6041
6051
|
deviceId: external_exports.string().min(1),
|
|
6042
6052
|
pinned: external_exports.boolean()
|
|
6043
6053
|
}).strict();
|
|
6054
|
+
ContactSetNoteArgsSchema = external_exports.object({
|
|
6055
|
+
deviceId: external_exports.string().min(1),
|
|
6056
|
+
/** 空串 = 清空备注 */
|
|
6057
|
+
note: external_exports.string()
|
|
6058
|
+
}).strict();
|
|
6059
|
+
ContactSetNoteOkSchema = external_exports.object({
|
|
6060
|
+
type: external_exports.literal("contact:setNote:ok"),
|
|
6061
|
+
deviceId: external_exports.string().min(1),
|
|
6062
|
+
note: external_exports.string()
|
|
6063
|
+
}).strict();
|
|
6044
6064
|
ContactPinOkSchema = external_exports.object({
|
|
6045
6065
|
type: external_exports.literal("contact:pin:ok"),
|
|
6046
6066
|
deviceId: external_exports.string().min(1),
|
|
@@ -8805,6 +8825,10 @@ var init_methods = __esm({
|
|
|
8805
8825
|
summary: "\u7F6E\u9876 / \u53D6\u6D88\u7F6E\u9876\u8054\u7CFB\u4EBA",
|
|
8806
8826
|
args: ContactPinArgsSchema
|
|
8807
8827
|
},
|
|
8828
|
+
"contact:setNote": {
|
|
8829
|
+
summary: "\u7ED9\u8054\u7CFB\u4EBA\uFF08\u5BF9\u65B9\u8BBE\u5907\uFF09\u5199\u5907\u6CE8\u3002owner \u672C\u5730\u79C1\u8BB0\uFF0C\u4E0D\u540C\u6B65\u7ED9\u5BF9\u65B9\uFF1B\u4E00\u4EBA\u591A\u8BBE\u5907\u65F6\u7528\u6765\u533A\u5206\u54EA\u53F0\u662F\u54EA\u53F0",
|
|
8830
|
+
args: ContactSetNoteArgsSchema
|
|
8831
|
+
},
|
|
8808
8832
|
"contact:remove": {
|
|
8809
8833
|
summary: "\u5220\u9664\u8054\u7CFB\u4EBA\u5E76\u6E05\u6389\u5BF9\u5E94\u7684 inbox \u961F\u5217",
|
|
8810
8834
|
args: ContactRemoveArgsSchema
|
|
@@ -57412,6 +57436,18 @@ var ContactStore = class {
|
|
|
57412
57436
|
this.flush();
|
|
57413
57437
|
return true;
|
|
57414
57438
|
}
|
|
57439
|
+
/**
|
|
57440
|
+
* 更新单条 contact 的备注(2026-08-31,对齐 setPin pattern)。
|
|
57441
|
+
* @param note 空串 = 清空备注
|
|
57442
|
+
* @returns 是否命中:deviceId 不存在返 false;命中即 flush.
|
|
57443
|
+
*/
|
|
57444
|
+
setNote(deviceId, note) {
|
|
57445
|
+
const existing = this.contacts.get(deviceId);
|
|
57446
|
+
if (!existing) return false;
|
|
57447
|
+
this.contacts.set(deviceId, { ...existing, note });
|
|
57448
|
+
this.flush();
|
|
57449
|
+
return true;
|
|
57450
|
+
}
|
|
57415
57451
|
flush() {
|
|
57416
57452
|
const file = path45.join(this.dataDir, FILE_NAME);
|
|
57417
57453
|
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -59974,6 +60010,219 @@ async function upsertDeviceBinding(opts) {
|
|
|
59974
60010
|
}
|
|
59975
60011
|
}
|
|
59976
60012
|
|
|
60013
|
+
// src/contact/org-mesh.ts
|
|
60014
|
+
var OrgMeshClientError = class extends Error {
|
|
60015
|
+
constructor(code, message, cause) {
|
|
60016
|
+
super(message);
|
|
60017
|
+
this.code = code;
|
|
60018
|
+
this.cause = cause;
|
|
60019
|
+
this.name = "OrgMeshClientError";
|
|
60020
|
+
}
|
|
60021
|
+
code;
|
|
60022
|
+
cause;
|
|
60023
|
+
};
|
|
60024
|
+
var DEFAULT_TIMEOUT_MS4 = 8e3;
|
|
60025
|
+
async function fetchOrgPeers(opts) {
|
|
60026
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
60027
|
+
const ac = new AbortController();
|
|
60028
|
+
const timer = setTimeout(() => ac.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS4);
|
|
60029
|
+
let res;
|
|
60030
|
+
try {
|
|
60031
|
+
res = await doFetch(`${opts.apiUrl}/api/device-bindings/org-peers`, {
|
|
60032
|
+
method: "GET",
|
|
60033
|
+
signal: ac.signal,
|
|
60034
|
+
headers: {
|
|
60035
|
+
authorization: `Bearer ${opts.apiKey}`,
|
|
60036
|
+
"x-did": encodeURIComponent(opts.deviceId),
|
|
60037
|
+
"x-name": encodeURIComponent(opts.displayName),
|
|
60038
|
+
"x-scene": "daemon"
|
|
60039
|
+
}
|
|
60040
|
+
});
|
|
60041
|
+
} catch (err) {
|
|
60042
|
+
throw new OrgMeshClientError("NETWORK", `network error: ${err.message}`, err);
|
|
60043
|
+
} finally {
|
|
60044
|
+
clearTimeout(timer);
|
|
60045
|
+
}
|
|
60046
|
+
if (!res.ok) {
|
|
60047
|
+
let detail = "";
|
|
60048
|
+
try {
|
|
60049
|
+
detail = JSON.stringify(await res.json());
|
|
60050
|
+
} catch {
|
|
60051
|
+
}
|
|
60052
|
+
throw new OrgMeshClientError("API_ERROR", `org-peers failed: ${res.status} ${detail}`);
|
|
60053
|
+
}
|
|
60054
|
+
const body = await res.json();
|
|
60055
|
+
return body.items ?? [];
|
|
60056
|
+
}
|
|
60057
|
+
async function syncOrgMesh(deps) {
|
|
60058
|
+
const peers = await deps.fetchPeers();
|
|
60059
|
+
if (peers.length === 0) {
|
|
60060
|
+
deps.log("org-mesh: \u4E0D\u5728\u4EFB\u4F55\u7EC4\u7EC7\uFF08\u6216\u7EC4\u91CC\u53EA\u6709\u81EA\u5DF1\uFF09\uFF0C\u8DF3\u8FC7");
|
|
60061
|
+
return { peers: 0, attempted: 0, connected: 0, failed: 0 };
|
|
60062
|
+
}
|
|
60063
|
+
const targets = [];
|
|
60064
|
+
for (const p2 of peers) {
|
|
60065
|
+
for (const d of p2.devices) {
|
|
60066
|
+
if (deps.store.get(d.deviceId)) continue;
|
|
60067
|
+
targets.push({ deviceId: d.deviceId, displayName: p2.displayName });
|
|
60068
|
+
}
|
|
60069
|
+
}
|
|
60070
|
+
deps.log("org-mesh: \u5F00\u59CB\u540C\u6B65", { peers: peers.length, targets: targets.length });
|
|
60071
|
+
let connected = 0;
|
|
60072
|
+
let failed = 0;
|
|
60073
|
+
for (const t of targets) {
|
|
60074
|
+
try {
|
|
60075
|
+
await deps.connectToDevice({ deviceId: t.deviceId, name: t.displayName });
|
|
60076
|
+
connected += 1;
|
|
60077
|
+
deps.log("org-mesh: \u5DF2\u5EFA\u8054", { deviceId: t.deviceId, name: t.displayName });
|
|
60078
|
+
} catch (e) {
|
|
60079
|
+
failed += 1;
|
|
60080
|
+
deps.log("org-mesh: \u5EFA\u8054\u5931\u8D25\uFF0C\u8DF3\u8FC7", {
|
|
60081
|
+
deviceId: t.deviceId,
|
|
60082
|
+
name: t.displayName,
|
|
60083
|
+
reason: e.message
|
|
60084
|
+
});
|
|
60085
|
+
}
|
|
60086
|
+
}
|
|
60087
|
+
deps.log("org-mesh: \u540C\u6B65\u5B8C\u6210", { peers: peers.length, attempted: targets.length, connected, failed });
|
|
60088
|
+
return { peers: peers.length, attempted: targets.length, connected, failed };
|
|
60089
|
+
}
|
|
60090
|
+
|
|
60091
|
+
// src/handlers/device.ts
|
|
60092
|
+
init_protocol();
|
|
60093
|
+
function ensureOwner(ctx) {
|
|
60094
|
+
if (!ctx || ctx.principal.kind !== "owner") {
|
|
60095
|
+
throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "UNAUTHORIZED: device:* requires owner ctx");
|
|
60096
|
+
}
|
|
60097
|
+
}
|
|
60098
|
+
function rethrowLoginExpired(e) {
|
|
60099
|
+
if (e instanceof CentralClientError && e.code === "UNAUTHORIZED") {
|
|
60100
|
+
throw new ClawdError(
|
|
60101
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
60102
|
+
`${LOGIN_EXPIRED_ERROR_MARKER}: \u767B\u5F55\u51ED\u8BC1\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55`
|
|
60103
|
+
);
|
|
60104
|
+
}
|
|
60105
|
+
throw e;
|
|
60106
|
+
}
|
|
60107
|
+
function createConnectToDevice(deps) {
|
|
60108
|
+
const now = deps.now ?? Date.now;
|
|
60109
|
+
return async (args) => {
|
|
60110
|
+
const selfUrl = deps.selfUrl();
|
|
60111
|
+
if (!selfUrl) {
|
|
60112
|
+
throw new ClawdError(
|
|
60113
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
60114
|
+
"TUNNEL_REQUIRED: \u672C\u673A\u672A\u5F00 tunnel\uFF0C\u5F00\u542F\u540E\u91CD\u8BD5"
|
|
60115
|
+
);
|
|
60116
|
+
}
|
|
60117
|
+
const exchanged = await deps.exchange(args.deviceId).catch(rethrowLoginExpired);
|
|
60118
|
+
const url = args.url ?? exchanged.deviceUrl;
|
|
60119
|
+
if (!url) {
|
|
60120
|
+
throw new ClawdError(
|
|
60121
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
60122
|
+
"DEVICE_URL_UNKNOWN: \u5BF9\u65B9\u8BBE\u5907\u672A\u4E0A\u62A5\u5730\u5740\uFF08\u672A\u767B\u5F55\u6216\u672A\u5F00 tunnel\uFF09\uFF0C\u8BF7\u663E\u5F0F\u63D0\u4F9B url \u6216\u7B49\u5BF9\u65B9\u4E0A\u7EBF"
|
|
60123
|
+
);
|
|
60124
|
+
}
|
|
60125
|
+
let conn;
|
|
60126
|
+
try {
|
|
60127
|
+
conn = await deps.connectRemote({ url, token: exchanged.token });
|
|
60128
|
+
} catch (e) {
|
|
60129
|
+
throw new ClawdError(
|
|
60130
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
60131
|
+
`DEVICE_UNREACHABLE: ${e.message}`
|
|
60132
|
+
);
|
|
60133
|
+
}
|
|
60134
|
+
try {
|
|
60135
|
+
let hs;
|
|
60136
|
+
try {
|
|
60137
|
+
hs = await conn.handshake(selfUrl);
|
|
60138
|
+
} catch (e) {
|
|
60139
|
+
const msg = e.message;
|
|
60140
|
+
if (/unknown method|METHOD_NOT_ALLOWED/i.test(msg)) {
|
|
60141
|
+
throw new ClawdError(
|
|
60142
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
60143
|
+
"REVERSE_UNSUPPORTED: \u5BF9\u65B9 clawd \u7248\u672C\u8FC7\u65E7\uFF0C\u8BF7\u8BA9\u5BF9\u65B9\u5347\u7EA7\u540E\u91CD\u8BD5"
|
|
60144
|
+
);
|
|
60145
|
+
}
|
|
60146
|
+
if (msg.includes(PEER_LOGIN_EXPIRED_ERROR_MARKER)) {
|
|
60147
|
+
throw new ClawdError(
|
|
60148
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
60149
|
+
`${PEER_LOGIN_EXPIRED_ERROR_MARKER}: \u5BF9\u65B9\u8BBE\u5907\u767B\u5F55\u5DF2\u8FC7\u671F\u6216\u672A\u767B\u5F55\uFF0C\u8BF7\u5BF9\u65B9\u5728 clawd \u91CD\u65B0\u767B\u5F55\u540E\u518D\u8BD5`
|
|
60150
|
+
);
|
|
60151
|
+
}
|
|
60152
|
+
throw e;
|
|
60153
|
+
}
|
|
60154
|
+
const existing = deps.store.get(args.deviceId) ?? void 0;
|
|
60155
|
+
const contact = {
|
|
60156
|
+
deviceId: args.deviceId,
|
|
60157
|
+
ownerId: hs.ownerId,
|
|
60158
|
+
provider: hs.provider,
|
|
60159
|
+
// 显示名优先用调用方传的 name:公共目录点击连接带 clawd_devices.name(对外展示名,
|
|
60160
|
+
// 如"公共1");"输入标识"连接不带 name → 回落 handshake 报的对方 owner 真名。
|
|
60161
|
+
// 归属人身份不丢(ownerId/provider 仍来自 handshake 背书)。
|
|
60162
|
+
displayName: args.name || hs.displayName || args.deviceId,
|
|
60163
|
+
remoteUrl: url,
|
|
60164
|
+
// connectToken 存自包含签名 token(断线重连复用,30 天有效;
|
|
60165
|
+
// 过期后对方拒 TOKEN_EXPIRED → 重新 device:connect 换新票)
|
|
60166
|
+
connectToken: exchanged.token,
|
|
60167
|
+
grants: hs.grants,
|
|
60168
|
+
addedAt: now(),
|
|
60169
|
+
pinnedAt: existing?.pinnedAt ?? null,
|
|
60170
|
+
remoteAccessAllowed: existing?.remoteAccessAllowed ?? false,
|
|
60171
|
+
// 备注是 owner 已配置状态,重复建联 upsert 必须保留(同 pinnedAt / remoteAccessAllowed)
|
|
60172
|
+
note: existing?.note ?? ""
|
|
60173
|
+
};
|
|
60174
|
+
deps.store.upsert(contact);
|
|
60175
|
+
deps.broadcast({ type: "contact:added", contact });
|
|
60176
|
+
return contact;
|
|
60177
|
+
} finally {
|
|
60178
|
+
try {
|
|
60179
|
+
conn.close();
|
|
60180
|
+
} catch {
|
|
60181
|
+
}
|
|
60182
|
+
}
|
|
60183
|
+
};
|
|
60184
|
+
}
|
|
60185
|
+
function buildDeviceHandlers(deps) {
|
|
60186
|
+
const connectToDevice = createConnectToDevice(deps);
|
|
60187
|
+
const list = async (_frame, _client, ctx) => {
|
|
60188
|
+
ensureOwner(ctx);
|
|
60189
|
+
const devices = await deps.listDevices().catch(rethrowLoginExpired);
|
|
60190
|
+
return {
|
|
60191
|
+
response: { type: "device:list:ok", devices }
|
|
60192
|
+
};
|
|
60193
|
+
};
|
|
60194
|
+
const connect = async (frame, _client, ctx) => {
|
|
60195
|
+
ensureOwner(ctx);
|
|
60196
|
+
const { type: _t, requestId: _r, ...rest } = frame;
|
|
60197
|
+
const args = DeviceConnectArgsSchema.parse(rest);
|
|
60198
|
+
const contact = await connectToDevice(args);
|
|
60199
|
+
return {
|
|
60200
|
+
response: {
|
|
60201
|
+
type: "device:connect:ok",
|
|
60202
|
+
// 返回连接的设备标识(= 请求的 args.deviceId)
|
|
60203
|
+
deviceId: args.deviceId,
|
|
60204
|
+
name: contact.displayName,
|
|
60205
|
+
url: contact.remoteUrl
|
|
60206
|
+
}
|
|
60207
|
+
};
|
|
60208
|
+
};
|
|
60209
|
+
const connections = async (_frame, client, ctx) => {
|
|
60210
|
+
ensureOwner(ctx);
|
|
60211
|
+
const entries = deps.listOwnerConnections();
|
|
60212
|
+
return {
|
|
60213
|
+
response: {
|
|
60214
|
+
type: "device:connections:ok",
|
|
60215
|
+
connections: entries.map((e) => ({ ...e, self: e.clientId === client.id }))
|
|
60216
|
+
}
|
|
60217
|
+
};
|
|
60218
|
+
};
|
|
60219
|
+
return {
|
|
60220
|
+
"device:list": list,
|
|
60221
|
+
"device:connect": connect,
|
|
60222
|
+
"device:connections": connections
|
|
60223
|
+
};
|
|
60224
|
+
}
|
|
60225
|
+
|
|
59977
60226
|
// src/feishu-auth/verify-token.ts
|
|
59978
60227
|
var crypto11 = __toESM(require("crypto"), 1);
|
|
59979
60228
|
var CONNECT_TOKEN_PREFIX = "clawdtk1";
|
|
@@ -61655,7 +61904,9 @@ function upsertPeerContact(args) {
|
|
|
61655
61904
|
grants,
|
|
61656
61905
|
addedAt: now(),
|
|
61657
61906
|
pinnedAt: existing?.pinnedAt ?? null,
|
|
61658
|
-
remoteAccessAllowed: existing?.remoteAccessAllowed ?? false
|
|
61907
|
+
remoteAccessAllowed: existing?.remoteAccessAllowed ?? false,
|
|
61908
|
+
// 备注是 owner 已配置状态,重复建联 upsert 必须保留(同 pinnedAt / remoteAccessAllowed)
|
|
61909
|
+
note: existing?.note ?? ""
|
|
61659
61910
|
};
|
|
61660
61911
|
args.store.upsert(base);
|
|
61661
61912
|
args.broadcast({ type: "contact:added", contact: base });
|
|
@@ -61663,7 +61914,7 @@ function upsertPeerContact(args) {
|
|
|
61663
61914
|
}
|
|
61664
61915
|
|
|
61665
61916
|
// src/handlers/contact.ts
|
|
61666
|
-
function
|
|
61917
|
+
function ensureOwner2(ctx) {
|
|
61667
61918
|
if (!ctx || ctx.principal.kind !== "owner") {
|
|
61668
61919
|
throw new ClawdError(
|
|
61669
61920
|
ERROR_CODES.UNAUTHORIZED,
|
|
@@ -61673,7 +61924,7 @@ function ensureOwner(ctx) {
|
|
|
61673
61924
|
}
|
|
61674
61925
|
function buildContactHandlers(deps) {
|
|
61675
61926
|
const list = async (_frame, _client, ctx) => {
|
|
61676
|
-
|
|
61927
|
+
ensureOwner2(ctx);
|
|
61677
61928
|
return {
|
|
61678
61929
|
response: {
|
|
61679
61930
|
type: "contact:list:ok",
|
|
@@ -61682,7 +61933,7 @@ function buildContactHandlers(deps) {
|
|
|
61682
61933
|
};
|
|
61683
61934
|
};
|
|
61684
61935
|
const pin = async (frame, _client, ctx) => {
|
|
61685
|
-
|
|
61936
|
+
ensureOwner2(ctx);
|
|
61686
61937
|
const args = parseArgs2(ContactPinArgsSchema, frame, "contact:pin");
|
|
61687
61938
|
const pinnedAt = args.pinned ? deps.now() : null;
|
|
61688
61939
|
const hit = deps.store.setPin(args.deviceId, pinnedAt);
|
|
@@ -61705,8 +61956,31 @@ function buildContactHandlers(deps) {
|
|
|
61705
61956
|
}
|
|
61706
61957
|
};
|
|
61707
61958
|
};
|
|
61959
|
+
const setNote = async (frame, _client, ctx) => {
|
|
61960
|
+
ensureOwner2(ctx);
|
|
61961
|
+
const args = parseArgs2(ContactSetNoteArgsSchema, frame, "contact:setNote");
|
|
61962
|
+
const hit = deps.store.setNote(args.deviceId, args.note);
|
|
61963
|
+
if (!hit) {
|
|
61964
|
+
throw new ClawdError(
|
|
61965
|
+
ERROR_CODES.CONTACT_NOT_FOUND,
|
|
61966
|
+
`CONTACT_NOT_FOUND: contact ${args.deviceId} not in store`
|
|
61967
|
+
);
|
|
61968
|
+
}
|
|
61969
|
+
deps.broadcast({
|
|
61970
|
+
type: "contact:note-updated",
|
|
61971
|
+
deviceId: args.deviceId,
|
|
61972
|
+
note: args.note
|
|
61973
|
+
});
|
|
61974
|
+
return {
|
|
61975
|
+
response: {
|
|
61976
|
+
type: "contact:setNote:ok",
|
|
61977
|
+
deviceId: args.deviceId,
|
|
61978
|
+
note: args.note
|
|
61979
|
+
}
|
|
61980
|
+
};
|
|
61981
|
+
};
|
|
61708
61982
|
const remove = async (frame, _client, ctx) => {
|
|
61709
|
-
|
|
61983
|
+
ensureOwner2(ctx);
|
|
61710
61984
|
const args = parseArgs2(ContactRemoveArgsSchema, frame, "contact:remove");
|
|
61711
61985
|
deps.store.removeByDeviceId(args.deviceId);
|
|
61712
61986
|
deps.broadcast({
|
|
@@ -61771,6 +62045,7 @@ function buildContactHandlers(deps) {
|
|
|
61771
62045
|
return {
|
|
61772
62046
|
"contact:list": list,
|
|
61773
62047
|
"contact:pin": pin,
|
|
62048
|
+
"contact:setNote": setNote,
|
|
61774
62049
|
"contact:remove": remove,
|
|
61775
62050
|
"contact:handshake": handshake
|
|
61776
62051
|
};
|
|
@@ -61778,7 +62053,7 @@ function buildContactHandlers(deps) {
|
|
|
61778
62053
|
|
|
61779
62054
|
// src/handlers/contact-remote-access.ts
|
|
61780
62055
|
init_protocol();
|
|
61781
|
-
function
|
|
62056
|
+
function ensureOwner3(ctx) {
|
|
61782
62057
|
if (!ctx || ctx.principal.kind !== "owner") {
|
|
61783
62058
|
throw new ClawdError(
|
|
61784
62059
|
ERROR_CODES.UNAUTHORIZED,
|
|
@@ -61788,7 +62063,7 @@ function ensureOwner2(ctx) {
|
|
|
61788
62063
|
}
|
|
61789
62064
|
function buildContactRemoteAccessHandlers(deps) {
|
|
61790
62065
|
const setRemoteAccess = async (frame, _client, ctx) => {
|
|
61791
|
-
|
|
62066
|
+
ensureOwner3(ctx);
|
|
61792
62067
|
const args = parseArgs2(ContactSetRemoteAccessArgsSchema, frame, "contact:setRemoteAccess");
|
|
61793
62068
|
const hit = deps.store.setRemoteAccess(args.deviceId, {
|
|
61794
62069
|
remoteAccessAllowed: args.remoteAccessAllowed
|
|
@@ -61921,132 +62196,6 @@ function buildFeishuAuthHandlers(deps) {
|
|
|
61921
62196
|
};
|
|
61922
62197
|
}
|
|
61923
62198
|
|
|
61924
|
-
// src/handlers/device.ts
|
|
61925
|
-
init_protocol();
|
|
61926
|
-
function ensureOwner3(ctx) {
|
|
61927
|
-
if (!ctx || ctx.principal.kind !== "owner") {
|
|
61928
|
-
throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "UNAUTHORIZED: device:* requires owner ctx");
|
|
61929
|
-
}
|
|
61930
|
-
}
|
|
61931
|
-
function rethrowLoginExpired(e) {
|
|
61932
|
-
if (e instanceof CentralClientError && e.code === "UNAUTHORIZED") {
|
|
61933
|
-
throw new ClawdError(
|
|
61934
|
-
ERROR_CODES.VALIDATION_ERROR,
|
|
61935
|
-
`${LOGIN_EXPIRED_ERROR_MARKER}: \u767B\u5F55\u51ED\u8BC1\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55`
|
|
61936
|
-
);
|
|
61937
|
-
}
|
|
61938
|
-
throw e;
|
|
61939
|
-
}
|
|
61940
|
-
function buildDeviceHandlers(deps) {
|
|
61941
|
-
const now = deps.now ?? Date.now;
|
|
61942
|
-
const list = async (_frame, _client, ctx) => {
|
|
61943
|
-
ensureOwner3(ctx);
|
|
61944
|
-
const devices = await deps.listDevices().catch(rethrowLoginExpired);
|
|
61945
|
-
return {
|
|
61946
|
-
response: { type: "device:list:ok", devices }
|
|
61947
|
-
};
|
|
61948
|
-
};
|
|
61949
|
-
const connect = async (frame, _client, ctx) => {
|
|
61950
|
-
ensureOwner3(ctx);
|
|
61951
|
-
const { type: _t, requestId: _r, ...rest } = frame;
|
|
61952
|
-
const args = DeviceConnectArgsSchema.parse(rest);
|
|
61953
|
-
const selfUrl = deps.selfUrl();
|
|
61954
|
-
if (!selfUrl) {
|
|
61955
|
-
throw new ClawdError(
|
|
61956
|
-
ERROR_CODES.VALIDATION_ERROR,
|
|
61957
|
-
"TUNNEL_REQUIRED: \u672C\u673A\u672A\u5F00 tunnel\uFF0C\u5F00\u542F\u540E\u91CD\u8BD5"
|
|
61958
|
-
);
|
|
61959
|
-
}
|
|
61960
|
-
const exchanged = await deps.exchange(args.deviceId).catch(rethrowLoginExpired);
|
|
61961
|
-
const url = args.url ?? exchanged.deviceUrl;
|
|
61962
|
-
if (!url) {
|
|
61963
|
-
throw new ClawdError(
|
|
61964
|
-
ERROR_CODES.VALIDATION_ERROR,
|
|
61965
|
-
"DEVICE_URL_UNKNOWN: \u5BF9\u65B9\u8BBE\u5907\u672A\u4E0A\u62A5\u5730\u5740\uFF08\u672A\u767B\u5F55\u6216\u672A\u5F00 tunnel\uFF09\uFF0C\u8BF7\u663E\u5F0F\u63D0\u4F9B url \u6216\u7B49\u5BF9\u65B9\u4E0A\u7EBF"
|
|
61966
|
-
);
|
|
61967
|
-
}
|
|
61968
|
-
let conn;
|
|
61969
|
-
try {
|
|
61970
|
-
conn = await deps.connectRemote({ url, token: exchanged.token });
|
|
61971
|
-
} catch (e) {
|
|
61972
|
-
throw new ClawdError(
|
|
61973
|
-
ERROR_CODES.VALIDATION_ERROR,
|
|
61974
|
-
`DEVICE_UNREACHABLE: ${e.message}`
|
|
61975
|
-
);
|
|
61976
|
-
}
|
|
61977
|
-
try {
|
|
61978
|
-
let hs;
|
|
61979
|
-
try {
|
|
61980
|
-
hs = await conn.handshake(selfUrl);
|
|
61981
|
-
} catch (e) {
|
|
61982
|
-
const msg = e.message;
|
|
61983
|
-
if (/unknown method|METHOD_NOT_ALLOWED/i.test(msg)) {
|
|
61984
|
-
throw new ClawdError(
|
|
61985
|
-
ERROR_CODES.VALIDATION_ERROR,
|
|
61986
|
-
"REVERSE_UNSUPPORTED: \u5BF9\u65B9 clawd \u7248\u672C\u8FC7\u65E7\uFF0C\u8BF7\u8BA9\u5BF9\u65B9\u5347\u7EA7\u540E\u91CD\u8BD5"
|
|
61987
|
-
);
|
|
61988
|
-
}
|
|
61989
|
-
if (msg.includes(PEER_LOGIN_EXPIRED_ERROR_MARKER)) {
|
|
61990
|
-
throw new ClawdError(
|
|
61991
|
-
ERROR_CODES.VALIDATION_ERROR,
|
|
61992
|
-
`${PEER_LOGIN_EXPIRED_ERROR_MARKER}: \u5BF9\u65B9\u8BBE\u5907\u767B\u5F55\u5DF2\u8FC7\u671F\u6216\u672A\u767B\u5F55\uFF0C\u8BF7\u5BF9\u65B9\u5728 clawd \u91CD\u65B0\u767B\u5F55\u540E\u518D\u8BD5`
|
|
61993
|
-
);
|
|
61994
|
-
}
|
|
61995
|
-
throw e;
|
|
61996
|
-
}
|
|
61997
|
-
const existing = deps.store.get(args.deviceId) ?? void 0;
|
|
61998
|
-
const contact = {
|
|
61999
|
-
deviceId: args.deviceId,
|
|
62000
|
-
ownerId: hs.ownerId,
|
|
62001
|
-
provider: hs.provider,
|
|
62002
|
-
// 显示名优先用调用方传的 name:公共目录点击连接带 clawd_devices.name(对外展示名,
|
|
62003
|
-
// 如"公共1");"输入标识"连接不带 name → 回落 handshake 报的对方 owner 真名。
|
|
62004
|
-
// 归属人身份不丢(ownerId/provider 仍来自 handshake 背书)。
|
|
62005
|
-
displayName: args.name || hs.displayName || args.deviceId,
|
|
62006
|
-
remoteUrl: url,
|
|
62007
|
-
// connectToken 存自包含签名 token(断线重连复用,30 天有效;
|
|
62008
|
-
// 过期后对方拒 TOKEN_EXPIRED → 重新 device:connect 换新票)
|
|
62009
|
-
connectToken: exchanged.token,
|
|
62010
|
-
grants: hs.grants,
|
|
62011
|
-
addedAt: now(),
|
|
62012
|
-
pinnedAt: existing?.pinnedAt ?? null,
|
|
62013
|
-
remoteAccessAllowed: existing?.remoteAccessAllowed ?? false
|
|
62014
|
-
};
|
|
62015
|
-
deps.store.upsert(contact);
|
|
62016
|
-
deps.broadcast({ type: "contact:added", contact });
|
|
62017
|
-
return {
|
|
62018
|
-
response: {
|
|
62019
|
-
type: "device:connect:ok",
|
|
62020
|
-
// 返回连接的设备标识(= 请求的 args.deviceId)
|
|
62021
|
-
deviceId: args.deviceId,
|
|
62022
|
-
name: contact.displayName,
|
|
62023
|
-
url
|
|
62024
|
-
}
|
|
62025
|
-
};
|
|
62026
|
-
} finally {
|
|
62027
|
-
try {
|
|
62028
|
-
conn.close();
|
|
62029
|
-
} catch {
|
|
62030
|
-
}
|
|
62031
|
-
}
|
|
62032
|
-
};
|
|
62033
|
-
const connections = async (_frame, client, ctx) => {
|
|
62034
|
-
ensureOwner3(ctx);
|
|
62035
|
-
const entries = deps.listOwnerConnections();
|
|
62036
|
-
return {
|
|
62037
|
-
response: {
|
|
62038
|
-
type: "device:connections:ok",
|
|
62039
|
-
connections: entries.map((e) => ({ ...e, self: e.clientId === client.id }))
|
|
62040
|
-
}
|
|
62041
|
-
};
|
|
62042
|
-
};
|
|
62043
|
-
return {
|
|
62044
|
-
"device:list": list,
|
|
62045
|
-
"device:connect": connect,
|
|
62046
|
-
"device:connections": connections
|
|
62047
|
-
};
|
|
62048
|
-
}
|
|
62049
|
-
|
|
62050
62199
|
// src/handlers/meta.ts
|
|
62051
62200
|
var import_node_os19 = __toESM(require("os"), 1);
|
|
62052
62201
|
init_protocol();
|
|
@@ -62081,6 +62230,7 @@ var METHOD_GRANT_MAP = {
|
|
|
62081
62230
|
// 写入路径在 device:connect / 自动反向;list / remove 作联系人读 / 删,owner-only。
|
|
62082
62231
|
"contact:list": ADMIN_ANY,
|
|
62083
62232
|
"contact:pin": ADMIN_ANY,
|
|
62233
|
+
"contact:setNote": ADMIN_ANY,
|
|
62084
62234
|
"contact:remove": ADMIN_ANY,
|
|
62085
62235
|
// contact:setRemoteAccess (owner UI 配远程访问授权):ADMIN_ANY
|
|
62086
62236
|
// (对齐 inbox:postMessage 的"能连上=有 auth,业务在 handler 校"模式)
|
|
@@ -62330,7 +62480,7 @@ function computeMethodAccess(args) {
|
|
|
62330
62480
|
}
|
|
62331
62481
|
|
|
62332
62482
|
// src/version.ts
|
|
62333
|
-
var version = "0.2.
|
|
62483
|
+
var version = "0.2.426".length > 0 ? "0.2.426" : "dev";
|
|
62334
62484
|
|
|
62335
62485
|
// src/cli-probe/probe.ts
|
|
62336
62486
|
var fs65 = __toESM(require("fs"), 1);
|
|
@@ -65168,11 +65318,11 @@ init_protocol();
|
|
|
65168
65318
|
// src/peer-ops/run-command.ts
|
|
65169
65319
|
var import_node_child_process18 = require("child_process");
|
|
65170
65320
|
var import_node_os23 = __toESM(require("os"), 1);
|
|
65171
|
-
var
|
|
65321
|
+
var DEFAULT_TIMEOUT_MS5 = 3e4;
|
|
65172
65322
|
var MAX_TIMEOUT_MS = 3e5;
|
|
65173
65323
|
var MAX_OUTPUT_BYTES = 1048576;
|
|
65174
65324
|
function runCommand(command, opts = {}) {
|
|
65175
|
-
const timeoutMs = Math.min(opts.timeoutMs ??
|
|
65325
|
+
const timeoutMs = Math.min(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS5, MAX_TIMEOUT_MS);
|
|
65176
65326
|
return new Promise((resolve9) => {
|
|
65177
65327
|
const child = (0, import_node_child_process18.spawn)("bash", ["-c", command], { cwd: import_node_os23.default.homedir() });
|
|
65178
65328
|
const out = [];
|
|
@@ -65476,11 +65626,11 @@ var IosInterestClientError = class extends Error {
|
|
|
65476
65626
|
code;
|
|
65477
65627
|
cause;
|
|
65478
65628
|
};
|
|
65479
|
-
var
|
|
65629
|
+
var DEFAULT_TIMEOUT_MS6 = 8e3;
|
|
65480
65630
|
async function submitIosInterest(opts) {
|
|
65481
65631
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
65482
65632
|
const ac = new AbortController();
|
|
65483
|
-
const timer = setTimeout(() => ac.abort(), opts.timeoutMs ??
|
|
65633
|
+
const timer = setTimeout(() => ac.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS6);
|
|
65484
65634
|
let res;
|
|
65485
65635
|
try {
|
|
65486
65636
|
res = await doFetch(`${opts.apiUrl}/api/ios-interest`, {
|
|
@@ -66813,6 +66963,30 @@ async function startDaemon(config) {
|
|
|
66813
66963
|
logger.warn("device register failed (best-effort)", { err: err.message });
|
|
66814
66964
|
}
|
|
66815
66965
|
};
|
|
66966
|
+
let orgMeshInFlight = false;
|
|
66967
|
+
const runOrgMesh = async () => {
|
|
66968
|
+
if (!feishuActive || orgMeshInFlight) return;
|
|
66969
|
+
const record3 = ownerIdentityStore.read();
|
|
66970
|
+
if (!record3) return;
|
|
66971
|
+
orgMeshInFlight = true;
|
|
66972
|
+
try {
|
|
66973
|
+
await syncOrgMesh({
|
|
66974
|
+
store: contactStore,
|
|
66975
|
+
fetchPeers: () => fetchOrgPeers({
|
|
66976
|
+
apiUrl: config.clawosApi ?? DEFAULT_CLAWOS_API,
|
|
66977
|
+
apiKey: resolveClawdTicketsApiKey(),
|
|
66978
|
+
deviceId: authFile.deviceId,
|
|
66979
|
+
displayName: record3.identity.displayName || authFile.deviceId
|
|
66980
|
+
}),
|
|
66981
|
+
connectToDevice: createConnectToDevice(feishuDeviceDeps),
|
|
66982
|
+
log: (msg, meta) => logger.info(msg, meta)
|
|
66983
|
+
});
|
|
66984
|
+
} catch (err) {
|
|
66985
|
+
logger.warn("org-mesh sync failed (best-effort)", { err: err.message });
|
|
66986
|
+
} finally {
|
|
66987
|
+
orgMeshInFlight = false;
|
|
66988
|
+
}
|
|
66989
|
+
};
|
|
66816
66990
|
const fetchServerKey = async () => {
|
|
66817
66991
|
if (!feishuActive) return;
|
|
66818
66992
|
try {
|
|
@@ -66831,6 +67005,41 @@ async function startDaemon(config) {
|
|
|
66831
67005
|
const publishedChannelStore = new PublishedChannelStore(publishedChannelsFile());
|
|
66832
67006
|
await publishedChannelStore.load();
|
|
66833
67007
|
const bundleCache = new BundleCache(bundleCacheRoot());
|
|
67008
|
+
const feishuDeviceDeps = {
|
|
67009
|
+
store: contactStore,
|
|
67010
|
+
exchange: async (deviceId) => {
|
|
67011
|
+
const record3 = ownerIdentityStore.read();
|
|
67012
|
+
if (!record3) {
|
|
67013
|
+
throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:connect requires feishu login");
|
|
67014
|
+
}
|
|
67015
|
+
return centralExchange({
|
|
67016
|
+
api: config.clawosApi ?? DEFAULT_CLAWOS_API,
|
|
67017
|
+
jwt: record3.ttcToken,
|
|
67018
|
+
provider: record3.identity.provider,
|
|
67019
|
+
deviceId,
|
|
67020
|
+
selfDeviceId: authFile.deviceId
|
|
67021
|
+
});
|
|
67022
|
+
},
|
|
67023
|
+
listDevices: async () => {
|
|
67024
|
+
const record3 = ownerIdentityStore.read();
|
|
67025
|
+
if (!record3) {
|
|
67026
|
+
throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:list requires feishu login");
|
|
67027
|
+
}
|
|
67028
|
+
return centralListDevices({
|
|
67029
|
+
api: config.clawosApi ?? DEFAULT_CLAWOS_API,
|
|
67030
|
+
ttcJwt: record3.ttcToken,
|
|
67031
|
+
provider: record3.identity.provider
|
|
67032
|
+
});
|
|
67033
|
+
},
|
|
67034
|
+
connectRemote,
|
|
67035
|
+
broadcast: (frame) => wsServer?.broadcastToOwners(frame),
|
|
67036
|
+
// 自动反向(spec 决策 #11):本机可达地址 = tunnel URL(公网),出站连接时
|
|
67037
|
+
// 透传让对方反向加我为联系人。无 tunnel(仅本机 ws)→ null,对方不自动反向(不造假数据)。
|
|
67038
|
+
// 决策 #16:身份不自报——本机 deviceId/ownerId/provider 由 exchange 签进 token 背书。
|
|
67039
|
+
selfUrl: () => currentTunnelUrl,
|
|
67040
|
+
// device:connections 数据源。惰性闭包:wsServer 晚于本 deps 赋值,请求时才解引用。
|
|
67041
|
+
listOwnerConnections: () => wsServer?.listOwnerConnections() ?? []
|
|
67042
|
+
};
|
|
66834
67043
|
const loginFlow = new LoginFlow({
|
|
66835
67044
|
store: ownerIdentityStore,
|
|
66836
67045
|
fetchUserInfo: (ttcToken) => fetchTtcUserInfo({ apiBase: TTC_HOSTS.api, token: ttcToken }),
|
|
@@ -67065,41 +67274,7 @@ async function startDaemon(config) {
|
|
|
67065
67274
|
// exchange/listDevices 包掉 owner ttcJwt(每次现读 store——热切换后立即用新身份);
|
|
67066
67275
|
// dispatcher 的 FEISHU_GATED_METHODS gate 已保证未登录时这些 RPC 到不了 handler,
|
|
67067
67276
|
// 这里的 FEISHU_LOGIN_REQUIRED 抛错只是防御纵深。
|
|
67068
|
-
feishuDevice:
|
|
67069
|
-
store: contactStore,
|
|
67070
|
-
exchange: async (deviceId) => {
|
|
67071
|
-
const record3 = ownerIdentityStore.read();
|
|
67072
|
-
if (!record3) {
|
|
67073
|
-
throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:connect requires feishu login");
|
|
67074
|
-
}
|
|
67075
|
-
return centralExchange({
|
|
67076
|
-
api: config.clawosApi ?? DEFAULT_CLAWOS_API,
|
|
67077
|
-
jwt: record3.ttcToken,
|
|
67078
|
-
provider: record3.identity.provider,
|
|
67079
|
-
deviceId,
|
|
67080
|
-
selfDeviceId: authFile.deviceId
|
|
67081
|
-
});
|
|
67082
|
-
},
|
|
67083
|
-
listDevices: async () => {
|
|
67084
|
-
const record3 = ownerIdentityStore.read();
|
|
67085
|
-
if (!record3) {
|
|
67086
|
-
throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:list requires feishu login");
|
|
67087
|
-
}
|
|
67088
|
-
return centralListDevices({
|
|
67089
|
-
api: config.clawosApi ?? DEFAULT_CLAWOS_API,
|
|
67090
|
-
ttcJwt: record3.ttcToken,
|
|
67091
|
-
provider: record3.identity.provider
|
|
67092
|
-
});
|
|
67093
|
-
},
|
|
67094
|
-
connectRemote,
|
|
67095
|
-
broadcast: (frame) => wsServer?.broadcastToOwners(frame),
|
|
67096
|
-
// 自动反向(spec 决策 #11):本机可达地址 = tunnel URL(公网),出站连接时
|
|
67097
|
-
// 透传让对方反向加我为联系人。无 tunnel(仅本机 ws)→ null,对方不自动反向(不造假数据)。
|
|
67098
|
-
// 决策 #16:身份不自报——本机 deviceId/ownerId/provider 由 exchange 签进 token 背书。
|
|
67099
|
-
selfUrl: () => currentTunnelUrl,
|
|
67100
|
-
// device:connections 数据源。惰性闭包:wsServer 晚于本 deps 赋值,请求时才解引用。
|
|
67101
|
-
listOwnerConnections: () => wsServer?.listOwnerConnections() ?? []
|
|
67102
|
-
},
|
|
67277
|
+
feishuDevice: feishuDeviceDeps,
|
|
67103
67278
|
// app-builder
|
|
67104
67279
|
appBuilderStore,
|
|
67105
67280
|
getProjectStore,
|
|
@@ -67400,6 +67575,7 @@ async function startDaemon(config) {
|
|
|
67400
67575
|
setImmediate(() => wsServer?.closeAllClients());
|
|
67401
67576
|
void reportDevice();
|
|
67402
67577
|
void fetchServerKey();
|
|
67578
|
+
void runOrgMesh();
|
|
67403
67579
|
} else {
|
|
67404
67580
|
logger.warn("feishu-login failed", {
|
|
67405
67581
|
reason: result.reason,
|
|
@@ -67606,6 +67782,7 @@ ${bar}
|
|
|
67606
67782
|
}
|
|
67607
67783
|
void reportDevice();
|
|
67608
67784
|
void fetchServerKey();
|
|
67785
|
+
void runOrgMesh();
|
|
67609
67786
|
const tickAttachmentGc = () => {
|
|
67610
67787
|
try {
|
|
67611
67788
|
runAttachmentGc({
|