@clawos-dev/clawd 0.2.424 → 0.2.425

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()}`;
@@ -59751,6 +59787,11 @@ var LoginFlow = class {
59751
59787
  ttcTokenExpiresAt: Number.isFinite(expiresAtNum) ? expiresAtNum : null,
59752
59788
  loginAt: new Date(now).toISOString()
59753
59789
  });
59790
+ void this.deps.syncOrgMesh?.().catch((err) => {
59791
+ this.deps.logWarn?.("org-mesh: \u767B\u5F55\u540E\u540C\u6B65\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u767B\u5F55\uFF09", {
59792
+ error: err instanceof Error ? err.message : String(err)
59793
+ });
59794
+ });
59754
59795
  return { ok: true, identity };
59755
59796
  }
59756
59797
  gcExpired(now) {
@@ -59974,6 +60015,219 @@ async function upsertDeviceBinding(opts) {
59974
60015
  }
59975
60016
  }
59976
60017
 
60018
+ // src/contact/org-mesh.ts
60019
+ var OrgMeshClientError = class extends Error {
60020
+ constructor(code, message, cause) {
60021
+ super(message);
60022
+ this.code = code;
60023
+ this.cause = cause;
60024
+ this.name = "OrgMeshClientError";
60025
+ }
60026
+ code;
60027
+ cause;
60028
+ };
60029
+ var DEFAULT_TIMEOUT_MS4 = 8e3;
60030
+ async function fetchOrgPeers(opts) {
60031
+ const doFetch = opts.fetchImpl ?? fetch;
60032
+ const ac = new AbortController();
60033
+ const timer = setTimeout(() => ac.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS4);
60034
+ let res;
60035
+ try {
60036
+ res = await doFetch(`${opts.apiUrl}/api/device-bindings/org-peers`, {
60037
+ method: "GET",
60038
+ signal: ac.signal,
60039
+ headers: {
60040
+ authorization: `Bearer ${opts.apiKey}`,
60041
+ "x-did": encodeURIComponent(opts.deviceId),
60042
+ "x-name": encodeURIComponent(opts.displayName),
60043
+ "x-scene": "daemon"
60044
+ }
60045
+ });
60046
+ } catch (err) {
60047
+ throw new OrgMeshClientError("NETWORK", `network error: ${err.message}`, err);
60048
+ } finally {
60049
+ clearTimeout(timer);
60050
+ }
60051
+ if (!res.ok) {
60052
+ let detail = "";
60053
+ try {
60054
+ detail = JSON.stringify(await res.json());
60055
+ } catch {
60056
+ }
60057
+ throw new OrgMeshClientError("API_ERROR", `org-peers failed: ${res.status} ${detail}`);
60058
+ }
60059
+ const body = await res.json();
60060
+ return body.items ?? [];
60061
+ }
60062
+ async function syncOrgMesh(deps) {
60063
+ const peers = await deps.fetchPeers();
60064
+ if (peers.length === 0) {
60065
+ deps.log("org-mesh: \u4E0D\u5728\u4EFB\u4F55\u7EC4\u7EC7\uFF08\u6216\u7EC4\u91CC\u53EA\u6709\u81EA\u5DF1\uFF09\uFF0C\u8DF3\u8FC7");
60066
+ return { peers: 0, attempted: 0, connected: 0, failed: 0 };
60067
+ }
60068
+ const targets = [];
60069
+ for (const p2 of peers) {
60070
+ for (const d of p2.devices) {
60071
+ if (deps.store.get(d.deviceId)) continue;
60072
+ targets.push({ deviceId: d.deviceId, displayName: p2.displayName });
60073
+ }
60074
+ }
60075
+ deps.log("org-mesh: \u5F00\u59CB\u540C\u6B65", { peers: peers.length, targets: targets.length });
60076
+ let connected = 0;
60077
+ let failed = 0;
60078
+ for (const t of targets) {
60079
+ try {
60080
+ await deps.connectToDevice({ deviceId: t.deviceId, name: t.displayName });
60081
+ connected += 1;
60082
+ deps.log("org-mesh: \u5DF2\u5EFA\u8054", { deviceId: t.deviceId, name: t.displayName });
60083
+ } catch (e) {
60084
+ failed += 1;
60085
+ deps.log("org-mesh: \u5EFA\u8054\u5931\u8D25\uFF0C\u8DF3\u8FC7", {
60086
+ deviceId: t.deviceId,
60087
+ name: t.displayName,
60088
+ reason: e.message
60089
+ });
60090
+ }
60091
+ }
60092
+ deps.log("org-mesh: \u540C\u6B65\u5B8C\u6210", { peers: peers.length, attempted: targets.length, connected, failed });
60093
+ return { peers: peers.length, attempted: targets.length, connected, failed };
60094
+ }
60095
+
60096
+ // src/handlers/device.ts
60097
+ init_protocol();
60098
+ function ensureOwner(ctx) {
60099
+ if (!ctx || ctx.principal.kind !== "owner") {
60100
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "UNAUTHORIZED: device:* requires owner ctx");
60101
+ }
60102
+ }
60103
+ function rethrowLoginExpired(e) {
60104
+ if (e instanceof CentralClientError && e.code === "UNAUTHORIZED") {
60105
+ throw new ClawdError(
60106
+ ERROR_CODES.VALIDATION_ERROR,
60107
+ `${LOGIN_EXPIRED_ERROR_MARKER}: \u767B\u5F55\u51ED\u8BC1\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55`
60108
+ );
60109
+ }
60110
+ throw e;
60111
+ }
60112
+ function createConnectToDevice(deps) {
60113
+ const now = deps.now ?? Date.now;
60114
+ return async (args) => {
60115
+ const selfUrl = deps.selfUrl();
60116
+ if (!selfUrl) {
60117
+ throw new ClawdError(
60118
+ ERROR_CODES.VALIDATION_ERROR,
60119
+ "TUNNEL_REQUIRED: \u672C\u673A\u672A\u5F00 tunnel\uFF0C\u5F00\u542F\u540E\u91CD\u8BD5"
60120
+ );
60121
+ }
60122
+ const exchanged = await deps.exchange(args.deviceId).catch(rethrowLoginExpired);
60123
+ const url = args.url ?? exchanged.deviceUrl;
60124
+ if (!url) {
60125
+ throw new ClawdError(
60126
+ ERROR_CODES.VALIDATION_ERROR,
60127
+ "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"
60128
+ );
60129
+ }
60130
+ let conn;
60131
+ try {
60132
+ conn = await deps.connectRemote({ url, token: exchanged.token });
60133
+ } catch (e) {
60134
+ throw new ClawdError(
60135
+ ERROR_CODES.VALIDATION_ERROR,
60136
+ `DEVICE_UNREACHABLE: ${e.message}`
60137
+ );
60138
+ }
60139
+ try {
60140
+ let hs;
60141
+ try {
60142
+ hs = await conn.handshake(selfUrl);
60143
+ } catch (e) {
60144
+ const msg = e.message;
60145
+ if (/unknown method|METHOD_NOT_ALLOWED/i.test(msg)) {
60146
+ throw new ClawdError(
60147
+ ERROR_CODES.VALIDATION_ERROR,
60148
+ "REVERSE_UNSUPPORTED: \u5BF9\u65B9 clawd \u7248\u672C\u8FC7\u65E7\uFF0C\u8BF7\u8BA9\u5BF9\u65B9\u5347\u7EA7\u540E\u91CD\u8BD5"
60149
+ );
60150
+ }
60151
+ if (msg.includes(PEER_LOGIN_EXPIRED_ERROR_MARKER)) {
60152
+ throw new ClawdError(
60153
+ ERROR_CODES.VALIDATION_ERROR,
60154
+ `${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`
60155
+ );
60156
+ }
60157
+ throw e;
60158
+ }
60159
+ const existing = deps.store.get(args.deviceId) ?? void 0;
60160
+ const contact = {
60161
+ deviceId: args.deviceId,
60162
+ ownerId: hs.ownerId,
60163
+ provider: hs.provider,
60164
+ // 显示名优先用调用方传的 name:公共目录点击连接带 clawd_devices.name(对外展示名,
60165
+ // 如"公共1");"输入标识"连接不带 name → 回落 handshake 报的对方 owner 真名。
60166
+ // 归属人身份不丢(ownerId/provider 仍来自 handshake 背书)。
60167
+ displayName: args.name || hs.displayName || args.deviceId,
60168
+ remoteUrl: url,
60169
+ // connectToken 存自包含签名 token(断线重连复用,30 天有效;
60170
+ // 过期后对方拒 TOKEN_EXPIRED → 重新 device:connect 换新票)
60171
+ connectToken: exchanged.token,
60172
+ grants: hs.grants,
60173
+ addedAt: now(),
60174
+ pinnedAt: existing?.pinnedAt ?? null,
60175
+ remoteAccessAllowed: existing?.remoteAccessAllowed ?? false,
60176
+ // 备注是 owner 已配置状态,重复建联 upsert 必须保留(同 pinnedAt / remoteAccessAllowed)
60177
+ note: existing?.note ?? ""
60178
+ };
60179
+ deps.store.upsert(contact);
60180
+ deps.broadcast({ type: "contact:added", contact });
60181
+ return contact;
60182
+ } finally {
60183
+ try {
60184
+ conn.close();
60185
+ } catch {
60186
+ }
60187
+ }
60188
+ };
60189
+ }
60190
+ function buildDeviceHandlers(deps) {
60191
+ const connectToDevice = createConnectToDevice(deps);
60192
+ const list = async (_frame, _client, ctx) => {
60193
+ ensureOwner(ctx);
60194
+ const devices = await deps.listDevices().catch(rethrowLoginExpired);
60195
+ return {
60196
+ response: { type: "device:list:ok", devices }
60197
+ };
60198
+ };
60199
+ const connect = async (frame, _client, ctx) => {
60200
+ ensureOwner(ctx);
60201
+ const { type: _t, requestId: _r, ...rest } = frame;
60202
+ const args = DeviceConnectArgsSchema.parse(rest);
60203
+ const contact = await connectToDevice(args);
60204
+ return {
60205
+ response: {
60206
+ type: "device:connect:ok",
60207
+ // 返回连接的设备标识(= 请求的 args.deviceId)
60208
+ deviceId: args.deviceId,
60209
+ name: contact.displayName,
60210
+ url: contact.remoteUrl
60211
+ }
60212
+ };
60213
+ };
60214
+ const connections = async (_frame, client, ctx) => {
60215
+ ensureOwner(ctx);
60216
+ const entries = deps.listOwnerConnections();
60217
+ return {
60218
+ response: {
60219
+ type: "device:connections:ok",
60220
+ connections: entries.map((e) => ({ ...e, self: e.clientId === client.id }))
60221
+ }
60222
+ };
60223
+ };
60224
+ return {
60225
+ "device:list": list,
60226
+ "device:connect": connect,
60227
+ "device:connections": connections
60228
+ };
60229
+ }
60230
+
59977
60231
  // src/feishu-auth/verify-token.ts
59978
60232
  var crypto11 = __toESM(require("crypto"), 1);
59979
60233
  var CONNECT_TOKEN_PREFIX = "clawdtk1";
@@ -61655,7 +61909,9 @@ function upsertPeerContact(args) {
61655
61909
  grants,
61656
61910
  addedAt: now(),
61657
61911
  pinnedAt: existing?.pinnedAt ?? null,
61658
- remoteAccessAllowed: existing?.remoteAccessAllowed ?? false
61912
+ remoteAccessAllowed: existing?.remoteAccessAllowed ?? false,
61913
+ // 备注是 owner 已配置状态,重复建联 upsert 必须保留(同 pinnedAt / remoteAccessAllowed)
61914
+ note: existing?.note ?? ""
61659
61915
  };
61660
61916
  args.store.upsert(base);
61661
61917
  args.broadcast({ type: "contact:added", contact: base });
@@ -61663,7 +61919,7 @@ function upsertPeerContact(args) {
61663
61919
  }
61664
61920
 
61665
61921
  // src/handlers/contact.ts
61666
- function ensureOwner(ctx) {
61922
+ function ensureOwner2(ctx) {
61667
61923
  if (!ctx || ctx.principal.kind !== "owner") {
61668
61924
  throw new ClawdError(
61669
61925
  ERROR_CODES.UNAUTHORIZED,
@@ -61673,7 +61929,7 @@ function ensureOwner(ctx) {
61673
61929
  }
61674
61930
  function buildContactHandlers(deps) {
61675
61931
  const list = async (_frame, _client, ctx) => {
61676
- ensureOwner(ctx);
61932
+ ensureOwner2(ctx);
61677
61933
  return {
61678
61934
  response: {
61679
61935
  type: "contact:list:ok",
@@ -61682,7 +61938,7 @@ function buildContactHandlers(deps) {
61682
61938
  };
61683
61939
  };
61684
61940
  const pin = async (frame, _client, ctx) => {
61685
- ensureOwner(ctx);
61941
+ ensureOwner2(ctx);
61686
61942
  const args = parseArgs2(ContactPinArgsSchema, frame, "contact:pin");
61687
61943
  const pinnedAt = args.pinned ? deps.now() : null;
61688
61944
  const hit = deps.store.setPin(args.deviceId, pinnedAt);
@@ -61705,8 +61961,31 @@ function buildContactHandlers(deps) {
61705
61961
  }
61706
61962
  };
61707
61963
  };
61964
+ const setNote = async (frame, _client, ctx) => {
61965
+ ensureOwner2(ctx);
61966
+ const args = parseArgs2(ContactSetNoteArgsSchema, frame, "contact:setNote");
61967
+ const hit = deps.store.setNote(args.deviceId, args.note);
61968
+ if (!hit) {
61969
+ throw new ClawdError(
61970
+ ERROR_CODES.CONTACT_NOT_FOUND,
61971
+ `CONTACT_NOT_FOUND: contact ${args.deviceId} not in store`
61972
+ );
61973
+ }
61974
+ deps.broadcast({
61975
+ type: "contact:note-updated",
61976
+ deviceId: args.deviceId,
61977
+ note: args.note
61978
+ });
61979
+ return {
61980
+ response: {
61981
+ type: "contact:setNote:ok",
61982
+ deviceId: args.deviceId,
61983
+ note: args.note
61984
+ }
61985
+ };
61986
+ };
61708
61987
  const remove = async (frame, _client, ctx) => {
61709
- ensureOwner(ctx);
61988
+ ensureOwner2(ctx);
61710
61989
  const args = parseArgs2(ContactRemoveArgsSchema, frame, "contact:remove");
61711
61990
  deps.store.removeByDeviceId(args.deviceId);
61712
61991
  deps.broadcast({
@@ -61771,6 +62050,7 @@ function buildContactHandlers(deps) {
61771
62050
  return {
61772
62051
  "contact:list": list,
61773
62052
  "contact:pin": pin,
62053
+ "contact:setNote": setNote,
61774
62054
  "contact:remove": remove,
61775
62055
  "contact:handshake": handshake
61776
62056
  };
@@ -61778,7 +62058,7 @@ function buildContactHandlers(deps) {
61778
62058
 
61779
62059
  // src/handlers/contact-remote-access.ts
61780
62060
  init_protocol();
61781
- function ensureOwner2(ctx) {
62061
+ function ensureOwner3(ctx) {
61782
62062
  if (!ctx || ctx.principal.kind !== "owner") {
61783
62063
  throw new ClawdError(
61784
62064
  ERROR_CODES.UNAUTHORIZED,
@@ -61788,7 +62068,7 @@ function ensureOwner2(ctx) {
61788
62068
  }
61789
62069
  function buildContactRemoteAccessHandlers(deps) {
61790
62070
  const setRemoteAccess = async (frame, _client, ctx) => {
61791
- ensureOwner2(ctx);
62071
+ ensureOwner3(ctx);
61792
62072
  const args = parseArgs2(ContactSetRemoteAccessArgsSchema, frame, "contact:setRemoteAccess");
61793
62073
  const hit = deps.store.setRemoteAccess(args.deviceId, {
61794
62074
  remoteAccessAllowed: args.remoteAccessAllowed
@@ -61921,132 +62201,6 @@ function buildFeishuAuthHandlers(deps) {
61921
62201
  };
61922
62202
  }
61923
62203
 
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
62204
  // src/handlers/meta.ts
62051
62205
  var import_node_os19 = __toESM(require("os"), 1);
62052
62206
  init_protocol();
@@ -62081,6 +62235,7 @@ var METHOD_GRANT_MAP = {
62081
62235
  // 写入路径在 device:connect / 自动反向;list / remove 作联系人读 / 删,owner-only。
62082
62236
  "contact:list": ADMIN_ANY,
62083
62237
  "contact:pin": ADMIN_ANY,
62238
+ "contact:setNote": ADMIN_ANY,
62084
62239
  "contact:remove": ADMIN_ANY,
62085
62240
  // contact:setRemoteAccess (owner UI 配远程访问授权):ADMIN_ANY
62086
62241
  // (对齐 inbox:postMessage 的"能连上=有 auth,业务在 handler 校"模式)
@@ -62330,7 +62485,7 @@ function computeMethodAccess(args) {
62330
62485
  }
62331
62486
 
62332
62487
  // src/version.ts
62333
- var version = "0.2.424".length > 0 ? "0.2.424" : "dev";
62488
+ var version = "0.2.425".length > 0 ? "0.2.425" : "dev";
62334
62489
 
62335
62490
  // src/cli-probe/probe.ts
62336
62491
  var fs65 = __toESM(require("fs"), 1);
@@ -65168,11 +65323,11 @@ init_protocol();
65168
65323
  // src/peer-ops/run-command.ts
65169
65324
  var import_node_child_process18 = require("child_process");
65170
65325
  var import_node_os23 = __toESM(require("os"), 1);
65171
- var DEFAULT_TIMEOUT_MS4 = 3e4;
65326
+ var DEFAULT_TIMEOUT_MS5 = 3e4;
65172
65327
  var MAX_TIMEOUT_MS = 3e5;
65173
65328
  var MAX_OUTPUT_BYTES = 1048576;
65174
65329
  function runCommand(command, opts = {}) {
65175
- const timeoutMs = Math.min(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS4, MAX_TIMEOUT_MS);
65330
+ const timeoutMs = Math.min(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS5, MAX_TIMEOUT_MS);
65176
65331
  return new Promise((resolve9) => {
65177
65332
  const child = (0, import_node_child_process18.spawn)("bash", ["-c", command], { cwd: import_node_os23.default.homedir() });
65178
65333
  const out = [];
@@ -65476,11 +65631,11 @@ var IosInterestClientError = class extends Error {
65476
65631
  code;
65477
65632
  cause;
65478
65633
  };
65479
- var DEFAULT_TIMEOUT_MS5 = 8e3;
65634
+ var DEFAULT_TIMEOUT_MS6 = 8e3;
65480
65635
  async function submitIosInterest(opts) {
65481
65636
  const doFetch = opts.fetchImpl ?? fetch;
65482
65637
  const ac = new AbortController();
65483
- const timer = setTimeout(() => ac.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS5);
65638
+ const timer = setTimeout(() => ac.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS6);
65484
65639
  let res;
65485
65640
  try {
65486
65641
  res = await doFetch(`${opts.apiUrl}/api/ios-interest`, {
@@ -66831,6 +66986,41 @@ async function startDaemon(config) {
66831
66986
  const publishedChannelStore = new PublishedChannelStore(publishedChannelsFile());
66832
66987
  await publishedChannelStore.load();
66833
66988
  const bundleCache = new BundleCache(bundleCacheRoot());
66989
+ const feishuDeviceDeps = {
66990
+ store: contactStore,
66991
+ exchange: async (deviceId) => {
66992
+ const record3 = ownerIdentityStore.read();
66993
+ if (!record3) {
66994
+ throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:connect requires feishu login");
66995
+ }
66996
+ return centralExchange({
66997
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
66998
+ jwt: record3.ttcToken,
66999
+ provider: record3.identity.provider,
67000
+ deviceId,
67001
+ selfDeviceId: authFile.deviceId
67002
+ });
67003
+ },
67004
+ listDevices: async () => {
67005
+ const record3 = ownerIdentityStore.read();
67006
+ if (!record3) {
67007
+ throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:list requires feishu login");
67008
+ }
67009
+ return centralListDevices({
67010
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
67011
+ ttcJwt: record3.ttcToken,
67012
+ provider: record3.identity.provider
67013
+ });
67014
+ },
67015
+ connectRemote,
67016
+ broadcast: (frame) => wsServer?.broadcastToOwners(frame),
67017
+ // 自动反向(spec 决策 #11):本机可达地址 = tunnel URL(公网),出站连接时
67018
+ // 透传让对方反向加我为联系人。无 tunnel(仅本机 ws)→ null,对方不自动反向(不造假数据)。
67019
+ // 决策 #16:身份不自报——本机 deviceId/ownerId/provider 由 exchange 签进 token 背书。
67020
+ selfUrl: () => currentTunnelUrl,
67021
+ // device:connections 数据源。惰性闭包:wsServer 晚于本 deps 赋值,请求时才解引用。
67022
+ listOwnerConnections: () => wsServer?.listOwnerConnections() ?? []
67023
+ };
66834
67024
  const loginFlow = new LoginFlow({
66835
67025
  store: ownerIdentityStore,
66836
67026
  fetchUserInfo: (ttcToken) => fetchTtcUserInfo({ apiBase: TTC_HOSTS.api, token: ttcToken }),
@@ -66859,7 +67049,23 @@ async function startDaemon(config) {
66859
67049
  });
66860
67050
  throw err;
66861
67051
  }
66862
- }
67052
+ },
67053
+ // org auto-mesh(spec 2026-08-31):登录成功后自动跟同组织的人建联。
67054
+ // 不进 fail-hard 链——login-flow 内部 void + catch,组网失败不拖垮登录。
67055
+ syncOrgMesh: () => syncOrgMesh({
67056
+ store: contactStore,
67057
+ fetchPeers: () => fetchOrgPeers({
67058
+ apiUrl: config.clawosApi ?? DEFAULT_CLAWOS_API,
67059
+ apiKey: resolveClawdTicketsApiKey(),
67060
+ deviceId: authFile.deviceId,
67061
+ displayName: ownerIdentityStore.read()?.identity.displayName ?? authFile.deviceId
67062
+ }),
67063
+ connectToDevice: createConnectToDevice(feishuDeviceDeps),
67064
+ log: (msg, meta) => logger.info(msg, meta)
67065
+ }),
67066
+ // 组网整体失败(拉清单挂了)的出口:走 logger 才进 SLS——
67067
+ // "自动组网悄悄没发生"正是要靠日志排查的场景,console.warn 只有 stdout。
67068
+ logWarn: (msg, meta) => logger.warn(msg, meta)
66863
67069
  });
66864
67070
  const visitorStore = createVisitorStore({ dir: config.dataDir });
66865
67071
  const exchangeVisitorToken = buildVisitorLogin({
@@ -67065,41 +67271,7 @@ async function startDaemon(config) {
67065
67271
  // exchange/listDevices 包掉 owner ttcJwt(每次现读 store——热切换后立即用新身份);
67066
67272
  // dispatcher 的 FEISHU_GATED_METHODS gate 已保证未登录时这些 RPC 到不了 handler,
67067
67273
  // 这里的 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
- },
67274
+ feishuDevice: feishuDeviceDeps,
67103
67275
  // app-builder
67104
67276
  appBuilderStore,
67105
67277
  getProjectStore,