dsh-lark-bot 0.19.13 → 0.19.15

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.js CHANGED
@@ -6190,7 +6190,7 @@ async function runDoctor(options) {
6190
6190
 
6191
6191
  // src/cli/commands/run.ts
6192
6192
  import { mkdir as mkdir22 } from "fs/promises";
6193
- import { randomUUID as randomUUID15 } from "crypto";
6193
+ import { randomUUID as randomUUID16 } from "crypto";
6194
6194
  import { join as join27 } from "path";
6195
6195
 
6196
6196
  // src/bot/active-runs.ts
@@ -11061,7 +11061,7 @@ async function usage(ctx) {
11061
11061
 
11062
11062
  // src/notify/sinks/channel-store.ts
11063
11063
  import { readFile as readFile23 } from "fs/promises";
11064
- var SINK_TYPES = ["telegram", "wecom"];
11064
+ var SINK_TYPES = ["telegram", "wecom", "wechat", "qq"];
11065
11065
  var NotificationChannelStore = class {
11066
11066
  constructor(path) {
11067
11067
  this.path = path;
@@ -11142,7 +11142,8 @@ function normalizeChannel(id, value) {
11142
11142
  if (!value || typeof value !== "object" || !id.trim()) return void 0;
11143
11143
  const input = value;
11144
11144
  const type = input.type;
11145
- if (type !== "telegram" && type !== "wecom") return void 0;
11145
+ if (typeof type !== "string" || !SINK_TYPES.includes(type)) return void 0;
11146
+ const normalizedType = type;
11146
11147
  const label2 = typeof input.label === "string" ? input.label.trim() : "";
11147
11148
  const destination = typeof input.destination === "string" ? input.destination.trim() : "";
11148
11149
  const secret = typeof input.secret === "string" ? input.secret.trim() : "";
@@ -11155,7 +11156,7 @@ function normalizeChannel(id, value) {
11155
11156
  }
11156
11157
  return {
11157
11158
  id: id.trim(),
11158
- type,
11159
+ type: normalizedType,
11159
11160
  label: label2,
11160
11161
  destination,
11161
11162
  secret,
@@ -11177,7 +11178,307 @@ function maskSecret(value) {
11177
11178
  return `${value.slice(0, 3)}\u2026${value.slice(-3)}`;
11178
11179
  }
11179
11180
 
11181
+ // src/onboard/sink-qr.ts
11182
+ var SinkQrRegistry = class {
11183
+ constructor(providers = []) {
11184
+ this.providers = providers;
11185
+ }
11186
+ providers;
11187
+ forType(type) {
11188
+ return this.providers.find((provider) => provider.type === type);
11189
+ }
11190
+ has(type) {
11191
+ return this.forType(type) !== void 0;
11192
+ }
11193
+ supportedTypes() {
11194
+ return this.providers.map((provider) => provider.type);
11195
+ }
11196
+ };
11197
+ async function pollUntilCompleted(provider, sessionId, options = {}) {
11198
+ const interval = options.intervalMs ?? 3e3;
11199
+ const timeout = options.timeoutMs ?? 9e4;
11200
+ const signal = options.signal;
11201
+ const deadline = Date.now() + timeout;
11202
+ let last = { phase: "pending" };
11203
+ while (Date.now() < deadline) {
11204
+ if (signal?.aborted) return { phase: "failed", error: "aborted" };
11205
+ last = await provider.poll(sessionId, signal);
11206
+ if (last.phase !== "pending") return last;
11207
+ await sleep3(interval, signal);
11208
+ }
11209
+ return { phase: "expired" };
11210
+ }
11211
+ function sleep3(ms, signal) {
11212
+ return new Promise((resolve6, reject) => {
11213
+ if (signal?.aborted) return reject(abortError());
11214
+ const timer = setTimeout(resolve6, ms);
11215
+ timer.unref?.();
11216
+ signal?.addEventListener(
11217
+ "abort",
11218
+ () => {
11219
+ clearTimeout(timer);
11220
+ reject(abortError());
11221
+ },
11222
+ { once: true }
11223
+ );
11224
+ });
11225
+ }
11226
+ function abortError() {
11227
+ const error = new Error("aborted");
11228
+ error.name = "AbortError";
11229
+ return error;
11230
+ }
11231
+
11232
+ // src/onboard/sink-qr-providers.ts
11233
+ import { createDecipheriv, randomBytes as randomBytes2, randomUUID as randomUUID8 } from "crypto";
11234
+
11235
+ // src/notify/sinks/text.ts
11236
+ function renderSinkText(message) {
11237
+ const zhLine = `${message.title.zh}\uFF08scope \`${message.scope}\`\uFF09${message.detail ? `\uFF1A${message.detail}` : ""}`;
11238
+ const enLine = `${message.title.en} (scope \`${message.scope}\`)${message.detail ? `: ${message.detail}` : ""}`;
11239
+ return `${stripMarkdown(zhLine)}
11240
+
11241
+ ${stripMarkdown(enLine)}`;
11242
+ }
11243
+ function escapeHtml(value) {
11244
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11245
+ }
11246
+ function stripMarkdown(value) {
11247
+ return value.replaceAll("`", "").replaceAll("**", "").replaceAll("*", "");
11248
+ }
11249
+
11250
+ // src/notify/sinks/wechat.ts
11251
+ var WeChatIlinkSink = class {
11252
+ constructor(baseUrl = "https://ilinkai.weixin.qq.com", randomUin = () => String(Math.floor(Math.random() * 1e9))) {
11253
+ this.baseUrl = baseUrl;
11254
+ this.randomUin = randomUin;
11255
+ }
11256
+ baseUrl;
11257
+ randomUin;
11258
+ type = "wechat";
11259
+ async send(channel, message) {
11260
+ const token = channel.secret;
11261
+ const [toUserId, contextToken] = splitDestination(channel.destination);
11262
+ if (!token || !toUserId || !contextToken) {
11263
+ log.warn("sink:wechat", "missing-credential", { channel: channel.id });
11264
+ return false;
11265
+ }
11266
+ const url = `${this.baseUrl}/ilink/bot/sendmessage`;
11267
+ const body3 = JSON.stringify({
11268
+ msg: {
11269
+ from_user_id: "",
11270
+ to_user_id: toUserId,
11271
+ client_id: "",
11272
+ message_type: 2,
11273
+ message_state: 2,
11274
+ item_list: [{ type: 1, text_item: { text: renderSinkText(message) } }],
11275
+ context_token: contextToken
11276
+ },
11277
+ base_info: { channel_version: 1 }
11278
+ });
11279
+ const headers = {
11280
+ "content-type": "application/json",
11281
+ authorization: `Bearer ${token}`,
11282
+ AuthorizationType: "ilink_bot_token",
11283
+ "X-WECHAT-UIN": this.randomUin(),
11284
+ "iLink-App-Id": "bot",
11285
+ "iLink-App-ClientVersion": "131072"
11286
+ };
11287
+ try {
11288
+ const controller = new AbortController();
11289
+ const timer = setTimeout(() => controller.abort(), 1e4);
11290
+ timer.unref?.();
11291
+ const response = await fetch(url, { method: "POST", headers, body: body3, signal: controller.signal });
11292
+ clearTimeout(timer);
11293
+ const payload = await response.json().catch(() => ({}));
11294
+ const ok = response.ok && (payload.ret === 0 || payload.ret === void 0);
11295
+ if (!ok) {
11296
+ log.warn("sink:wechat", "send-failed", {
11297
+ channel: channel.id,
11298
+ error: payload.msg ?? payload.errmsg ?? `http ${response.status}`
11299
+ });
11300
+ return false;
11301
+ }
11302
+ return true;
11303
+ } catch (error) {
11304
+ log.warn("sink:wechat", "send-error", { channel: channel.id, error });
11305
+ return false;
11306
+ }
11307
+ }
11308
+ };
11309
+ function splitDestination(value) {
11310
+ const sep3 = value.indexOf("|");
11311
+ if (sep3 === -1) return [value, ""];
11312
+ return [value.slice(0, sep3), value.slice(sep3 + 1)];
11313
+ }
11314
+
11315
+ // src/onboard/sink-qr-providers.ts
11316
+ var API_HEADERS = { "content-type": "application/json", accept: "application/json" };
11317
+ var BIND_STATUS = { NONE: 0, PENDING: 1, COMPLETED: 2, EXPIRED: 3 };
11318
+ var QqQrProvider = class {
11319
+ constructor(portalHost = process.env.DSH_LARK_QQ_PORTAL_HOST ?? "q.qq.com", fetchImpl = fetch, genKey = () => randomBytes2(32).toString("base64")) {
11320
+ this.portalHost = portalHost;
11321
+ this.fetchImpl = fetchImpl;
11322
+ this.genKey = genKey;
11323
+ }
11324
+ portalHost;
11325
+ fetchImpl;
11326
+ genKey;
11327
+ type = "qq";
11328
+ sessions = /* @__PURE__ */ new Map();
11329
+ async begin(_options) {
11330
+ const aesKey = this.genKey();
11331
+ const response = await this.fetchImpl(`https://${this.portalHost}/lite/create_bind_task`, {
11332
+ method: "POST",
11333
+ headers: API_HEADERS,
11334
+ body: JSON.stringify({ key: aesKey })
11335
+ });
11336
+ const payload = await response.json().catch(() => ({}));
11337
+ const taskId = payload.data?.task_id;
11338
+ if (!response.ok || !taskId || payload.retcode !== void 0 && payload.retcode !== 0) {
11339
+ throw new Error(`qq create_bind_task failed: ${payload.msg ?? ""} (http ${response.status})`);
11340
+ }
11341
+ const sessionId = randomUUID8();
11342
+ this.sessions.set(sessionId, { taskId, aesKey });
11343
+ const qrUrl = `https://q.qq.com/qqbot/openclaw/connect.html?task_id=${encodeURIComponent(taskId)}&_wv=2&source=dsh-lark-bot`;
11344
+ return { providerType: this.type, sessionId, qrUrl, expireIn: 600 };
11345
+ }
11346
+ async poll(sessionId, _signal) {
11347
+ const state = this.sessions.get(sessionId);
11348
+ if (!state) return { phase: "failed", error: "unknown session" };
11349
+ let data = {};
11350
+ try {
11351
+ const response = await this.fetchImpl(`https://${this.portalHost}/lite/poll_bind_result`, {
11352
+ method: "POST",
11353
+ headers: API_HEADERS,
11354
+ body: JSON.stringify({ task_id: state.taskId })
11355
+ });
11356
+ const payload = await response.json();
11357
+ if (!response.ok || payload.retcode !== void 0 && payload.retcode !== 0) return { phase: "pending" };
11358
+ data = payload.data ?? {};
11359
+ } catch {
11360
+ return { phase: "pending" };
11361
+ }
11362
+ const status = data.status ?? BIND_STATUS.NONE;
11363
+ if (status === BIND_STATUS.COMPLETED && data.bot_appid && data.bot_encrypt_secret) {
11364
+ let clientSecret;
11365
+ try {
11366
+ clientSecret = decryptSecret(data.bot_encrypt_secret, state.aesKey);
11367
+ } catch (error) {
11368
+ this.sessions.delete(sessionId);
11369
+ return { phase: "failed", error: `decrypt failed: ${error instanceof Error ? error.message : String(error)}` };
11370
+ }
11371
+ const target = data.user_openid ? `user:${data.user_openid}` : "";
11372
+ this.sessions.delete(sessionId);
11373
+ return {
11374
+ phase: "completed",
11375
+ channel: {
11376
+ id: "",
11377
+ type: "qq",
11378
+ label: "QQ",
11379
+ destination: target,
11380
+ secret: `${data.bot_appid}:${clientSecret}`
11381
+ }
11382
+ };
11383
+ }
11384
+ if (status === BIND_STATUS.EXPIRED) {
11385
+ this.sessions.delete(sessionId);
11386
+ return { phase: "expired" };
11387
+ }
11388
+ return { phase: "pending" };
11389
+ }
11390
+ };
11391
+ var WeChatQrProvider = class {
11392
+ constructor(baseUrl = process.env.DSH_LARK_WECHAT_ILINK_URL ?? "https://ilinkai.weixin.qq.com", fetchImpl = fetch) {
11393
+ this.baseUrl = baseUrl;
11394
+ this.fetchImpl = fetchImpl;
11395
+ }
11396
+ baseUrl;
11397
+ fetchImpl;
11398
+ type = "wechat";
11399
+ sessions = /* @__PURE__ */ new Map();
11400
+ async begin(_options) {
11401
+ const response = await this.fetchImpl(`${this.baseUrl}/ilink/bot/get_bot_qrcode?bot_type=3`, {
11402
+ method: "GET"
11403
+ });
11404
+ const payload = await response.json().catch(() => ({}));
11405
+ const qrcode2 = payload.qrcode ?? payload.url ?? "";
11406
+ if (!response.ok || !qrcode2) {
11407
+ throw new Error(payload.msg ?? `wechat get_bot_qrcode failed: http ${response.status}`);
11408
+ }
11409
+ const sessionId = randomUUID8();
11410
+ this.sessions.set(sessionId, { qrcode: qrcode2 });
11411
+ return { providerType: this.type, sessionId, qrUrl: qrcode2, expireIn: 180 };
11412
+ }
11413
+ async poll(sessionId, _signal) {
11414
+ const state = this.sessions.get(sessionId);
11415
+ if (!state) return { phase: "failed", error: "unknown session" };
11416
+ const statusUrl = `${this.baseUrl}/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(state.qrcode)}`;
11417
+ try {
11418
+ const response = await this.fetchImpl(statusUrl, {
11419
+ method: "GET",
11420
+ headers: { "iLink-App-Id": "bot", "iLink-App-ClientVersion": "131072" }
11421
+ });
11422
+ const payload = await response.json().catch(() => ({}));
11423
+ const status = String(payload.status ?? "wait").toLowerCase();
11424
+ if (status === "confirmed" || status === "success" || status === "scanned" || status === "2") {
11425
+ const token = payload.token ?? payload.access_token ?? "";
11426
+ const userId = payload.openid ?? payload.user_id ?? payload.to_user_id ?? "";
11427
+ this.sessions.delete(sessionId);
11428
+ return {
11429
+ phase: "completed",
11430
+ channel: { id: "", type: "wechat", label: "\u5FAE\u4FE1", destination: userId ? `${userId}|` : "", secret: token }
11431
+ };
11432
+ }
11433
+ if (status === "expired") {
11434
+ this.sessions.delete(sessionId);
11435
+ return { phase: "expired" };
11436
+ }
11437
+ return { phase: "pending" };
11438
+ } catch {
11439
+ return { phase: "pending" };
11440
+ }
11441
+ }
11442
+ };
11443
+ function buildSinkQrProviders() {
11444
+ return [new WeChatQrProvider(), new QqQrProvider()];
11445
+ }
11446
+ function coerceSinkQrChannel(input) {
11447
+ const destination = input.type === "wechat" ? normalizeWechatDestination(input.destination) : input.destination;
11448
+ return { id: input.id, type: input.type, label: input.label, destination, secret: input.secret };
11449
+ }
11450
+ function normalizeWechatDestination(value) {
11451
+ if (value.includes("|")) return value;
11452
+ const [id] = splitDestination(value);
11453
+ return id ? `${id}|` : value;
11454
+ }
11455
+ function decryptSecret(encryptedBase64, keyBase64) {
11456
+ const key = Buffer.from(keyBase64, "base64");
11457
+ const raw = Buffer.from(encryptedBase64, "base64");
11458
+ if (key.length !== 32) throw new Error("bind key must be 32 bytes");
11459
+ if (raw.length < 12 + 16) throw new Error("ciphertext too short");
11460
+ const iv = raw.subarray(0, 12);
11461
+ const body3 = raw.subarray(12);
11462
+ const tag = body3.subarray(body3.length - 16);
11463
+ const ciphertext = body3.subarray(0, body3.length - 16);
11464
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
11465
+ decipher.setAuthTag(tag);
11466
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
11467
+ }
11468
+
11469
+ // src/onboard/qr-image.ts
11470
+ import { toBuffer } from "qrcode";
11471
+ async function renderQrPng(value, options = {}) {
11472
+ return toBuffer(value, {
11473
+ type: "png",
11474
+ width: options.width ?? 400,
11475
+ margin: options.margin ?? 2,
11476
+ errorCorrectionLevel: "M"
11477
+ });
11478
+ }
11479
+
11180
11480
  // src/commands/channels.ts
11481
+ var QR_REGISTRY = new SinkQrRegistry(buildSinkQrProviders());
11181
11482
  async function reply6(ctx, zh, en) {
11182
11483
  await ctx.channel.sendMarkdown(ctx.chatId, bilingualMarkdown(zh, en), { replyTo: ctx.messageId });
11183
11484
  }
@@ -11206,6 +11507,8 @@ async function handleChannels(args, ctx) {
11206
11507
  }
11207
11508
  case "add":
11208
11509
  return addChannel(ctx, store, rest.join(" "));
11510
+ case "accept":
11511
+ return acceptChannel(ctx, store, rest.join(" "));
11209
11512
  case "remove": {
11210
11513
  const id = rest[0];
11211
11514
  if (!id) return usage2(ctx);
@@ -11250,6 +11553,7 @@ async function showChannel(ctx, channel) {
11250
11553
  await reply6(ctx, zh, en);
11251
11554
  }
11252
11555
  async function addChannel(ctx, store, raw) {
11556
+ if (raw.trim().startsWith("--qr")) return addChannelByQr(ctx, store, raw.trim());
11253
11557
  const parsed = parseAddArgs(raw);
11254
11558
  if (!parsed || !parsed.type || !parsed.label || !parsed.destination || !parsed.secret) {
11255
11559
  await usage2(ctx);
@@ -11275,14 +11579,116 @@ async function addChannel(ctx, store, raw) {
11275
11579
  await store.add(channel);
11276
11580
  await reply6(ctx, `\u5DF2\u6DFB\u52A0\u901A\u77E5\u6E20\u9053 ${maskChannel(channel)}\u3002`, `Added notification channel ${maskChannel(channel)}.`);
11277
11581
  }
11582
+ function platformName(type) {
11583
+ const names = {
11584
+ telegram: "Telegram",
11585
+ wecom: "\u4F01\u4E1A\u5FAE\u4FE1",
11586
+ wechat: "\u5FAE\u4FE1",
11587
+ qq: "QQ"
11588
+ };
11589
+ return names[type] ?? type;
11590
+ }
11591
+ async function addChannelByQr(ctx, store, raw) {
11592
+ const parsed = parseQrArgs(raw);
11593
+ const type = parsed.type;
11594
+ if (!type) {
11595
+ await reply6(ctx, "\u7528\u6CD5\uFF1A`/channels add --qr <wechat|qq|telegram> [--id <id>] [--label <label>]`", "Usage: `/channels add --qr <wechat|qq|telegram> [--id <id>] [--label <label>]`");
11596
+ return;
11597
+ }
11598
+ if (!SINK_TYPES.includes(type)) {
11599
+ await reply6(ctx, `\u4E0D\u652F\u6301\u7684\u6E20\u9053\u7C7B\u578B\uFF1A\`${type}\`\u3002\u53EF\u7528\uFF1A\`${SINK_TYPES.join("|")}\`\u3002`, `Unsupported channel type: \`${type}\`. Available: \`${SINK_TYPES.join("|")}\`.`);
11600
+ return;
11601
+ }
11602
+ const provider = QR_REGISTRY.forType(type);
11603
+ if (!provider) {
11604
+ await reply6(ctx, `\`${type}\` \u6682\u65E0\u626B\u7801\u7ED1\u5B9A\u652F\u6301\uFF0C\u8BF7\u6539\u7528 \`/channels add ${type} <label> --destination <\u76EE\u6807> --secret <\u5BC6\u94A5>\`\u3002`, `\`${type}\` has no QR binding yet. Use \`/channels add ${type} <label> --destination <target> --secret <token>\` instead.`);
11605
+ return;
11606
+ }
11607
+ const id = parsed.id ?? `${type}-qr-${slugify2(parsed.label ?? platformName(type))}`;
11608
+ if (store.get(id)) {
11609
+ await reply6(ctx, `\u901A\u77E5\u6E20\u9053 \`${id}\` \u5DF2\u5B58\u5728\u3002\u7528 \`/channels remove ${id}\` \u5220\u9664\u540E\u91CD\u5EFA\u3002`, `Notification channel \`${id}\` already exists. Use \`/channels remove ${id}\` to recreate it.`);
11610
+ return;
11611
+ }
11612
+ let session;
11613
+ try {
11614
+ session = await provider.begin({ id, label: parsed.label ?? platformName(type) });
11615
+ } catch (error) {
11616
+ const message = error instanceof Error ? error.message : String(error);
11617
+ await reply6(
11618
+ ctx,
11619
+ `\u65E0\u6CD5\u53D1\u8D77 ${platformName(type)} \u626B\u7801\u7ED1\u5B9A\uFF1A${message}\u3002\u8BF7\u6539\u7528\u624B\u52A8\u65B9\u5F0F \`/channels add ${type} <label> --destination <\u76EE\u6807> --secret <\u5BC6\u94A5>\`\u3002`,
11620
+ `Could not start a ${platformName(type)} QR bind: ${message}. Use manual entry \`/channels add ${type} <label> --destination <target> --secret <token>\` instead.`
11621
+ );
11622
+ return;
11623
+ }
11624
+ const png = await renderQrPng(session.qrUrl);
11625
+ const minutes = Math.max(1, Math.round(session.expireIn / 60));
11626
+ const introZh = `${platformName(type)}\u626B\u7801\u7ED1\u5B9A\uFF1A\u8BF7\u7528\u300C${platformName(type)}\u300DApp \u626B\u63CF\u4E0B\u65B9\u4E8C\u7EF4\u7801\u521B\u5EFA / \u7ED1\u5B9A\u901A\u77E5\u4F1A\u8BDD\uFF0C\u4E8C\u7EF4\u7801\u7EA6 ${minutes} \u5206\u949F\u6709\u6548\u3002\u626B\u7801\u540E\u7B49\u5F85\u81EA\u52A8\u5B8C\u6210\uFF0C\u6216\u8D85\u65F6\u540E\u7528 \`/channels accept ${type} ${id} ${parsed.label ?? platformName(type)} <\u76EE\u6807> <\u5BC6\u94A5>\`\u3002`;
11627
+ const introEn = `${platformName(type)} QR bind: scan the code below with the ${platformName(type)} app to create / bind the notification session. It stays valid for about ${minutes} minute(s). After scanning it auto-completes, or use \`/channels accept ${type} ${id} ${parsed.label ?? platformName(type)} <target> <token>\` on timeout.`;
11628
+ if (typeof ctx.channel.sendImage === "function") {
11629
+ await ctx.channel.sendImage(ctx.chatId, png, { replyTo: ctx.messageId });
11630
+ } else {
11631
+ await reply6(ctx, `\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u4E0D\u652F\u6301\u53D1\u9001\u56FE\u7247\uFF0C\u8BF7\u6D4F\u89C8\u5668\u6253\u5F00\uFF1A${session.qrUrl}`, `Image sending is unavailable here; open in a browser: ${session.qrUrl}`);
11632
+ }
11633
+ await reply6(ctx, introZh, introEn);
11634
+ const result = await pollUntilCompleted(provider, session.sessionId, { timeoutMs: 9e4 });
11635
+ if (result.phase === "completed" && result.channel) {
11636
+ const channel = {
11637
+ ...coerceSinkQrChannel({
11638
+ id,
11639
+ type,
11640
+ label: result.channel.label || parsed.label || platformName(type),
11641
+ destination: result.channel.destination,
11642
+ secret: result.channel.secret
11643
+ }),
11644
+ enabled: true
11645
+ };
11646
+ await store.add(channel);
11647
+ await reply6(ctx, `\u2705 \u5DF2\u7ED1\u5B9A\u5E76\u6DFB\u52A0\u901A\u77E5\u6E20\u9053 ${maskChannel(channel)}\u3002`, `\u2705 Bound and added notification channel ${maskChannel(channel)}.`);
11648
+ return;
11649
+ }
11650
+ const reason = result.phase === "expired" ? "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F" : result.error ?? "\u672A\u5728\u7B49\u5F85\u65F6\u95F4\u5185\u5B8C\u6210";
11651
+ await reply6(
11652
+ ctx,
11653
+ `\u672A\u5B8C\u6210${platformName(type)}\u626B\u7801\u7ED1\u5B9A\uFF08${reason}\uFF09\u3002\u8BF7\u91CD\u65B0\u7528 \`/channels add --qr ${type}\` \u53D1\u8D77\uFF0C\u6216\u624B\u52A8\u586B \`/channels add ${type} <label> --destination <\u76EE\u6807> --secret <\u5BC6\u94A5>\`\u3002`,
11654
+ `${platformName(type)} QR bind did not complete (${reason}). Retry \`/channels add --qr ${type}\`, or enter it manually with \`/channels add ${type} <label> --destination <target> --secret <token>\`.`
11655
+ );
11656
+ }
11657
+ async function acceptChannel(ctx, store, raw) {
11658
+ const [type, id, label2, destination, secret] = raw.trim().split(/\s+/);
11659
+ if (!type || !id || !label2 || !destination || !secret || !SINK_TYPES.includes(type)) {
11660
+ await reply6(ctx, "\u7528\u6CD5\uFF1A`/channels accept <type> <id> <label> <destination> <secret>`", "Usage: `/channels accept <type> <id> <label> <destination> <secret>`");
11661
+ return;
11662
+ }
11663
+ if (store.get(id)) {
11664
+ await reply6(ctx, `\u901A\u77E5\u6E20\u9053 \`${id}\` \u5DF2\u5B58\u5728\u3002\u7528 \`/channels remove ${id}\` \u5220\u9664\u540E\u91CD\u5EFA\u3002`, `Notification channel \`${id}\` already exists. Remove it first.`);
11665
+ return;
11666
+ }
11667
+ const channel = {
11668
+ ...coerceSinkQrChannel({ id, type, label: label2, destination, secret }),
11669
+ enabled: true
11670
+ };
11671
+ await store.add(channel);
11672
+ await reply6(ctx, `\u5DF2\u6DFB\u52A0\u901A\u77E5\u6E20\u9053 ${maskChannel(channel)}\u3002`, `Added notification channel ${maskChannel(channel)}.`);
11673
+ }
11674
+ function parseQrArgs(raw) {
11675
+ const typeMatch = raw.match(/--qr\s+(\S+)/);
11676
+ const idMatch = raw.match(/--id\s+(\S+)/);
11677
+ const labelMatch = raw.match(/--label\s+(.+?)(?:\s--|\s*$)/);
11678
+ return {
11679
+ ...typeMatch ? { type: typeMatch[1] } : {},
11680
+ ...idMatch ? { id: idMatch[1] } : {},
11681
+ ...labelMatch ? { label: labelMatch[1].trim() } : {}
11682
+ };
11683
+ }
11278
11684
  async function notFound(ctx, id) {
11279
11685
  await reply6(ctx, `\u672A\u627E\u5230\u901A\u77E5\u6E20\u9053 \`${id}\`\u3002\u7528 \`/channels list\` \u67E5\u770B\u3002`, `Unknown notification channel \`${id}\`. Use \`/channels list\`.`);
11280
11686
  }
11281
11687
  async function usage2(ctx) {
11282
11688
  await reply6(
11283
11689
  ctx,
11284
- "\u7528\u6CD5\uFF1A`/channels list|show <id>|add <telegram|wecom> <label> [--id <id>] --destination <\u76EE\u6807> --secret <\u5BC6\u94A5>|remove <id>|enable <id>|disable <id>`",
11285
- "Usage: `/channels list|show <id>|add <telegram|wecom> <label> [--id <id>] --destination <target> --secret <token>|remove <id>|enable <id>|disable <id>`"
11690
+ "\u7528\u6CD5\uFF1A`/channels list|show <id>|add --qr <wechat|qq|telegram> [--id <id>] [--label <label>]|add <type> <label> [--id <id>] --destination <\u76EE\u6807> --secret <\u5BC6\u94A5>|accept <type> <id> <label> <destination> <secret>|remove <id>|enable <id>|disable <id>`",
11691
+ "Usage: `/channels list|show <id>|add --qr <wechat|qq|telegram> [--id <id>] [--label <label>]|add <type> <label> [--id <id>] --destination <target> --secret <token>|accept <type> <id> <label> <destination> <secret>|remove <id>|enable <id>|disable <id>`"
11286
11692
  );
11287
11693
  }
11288
11694
  function parseAddArgs(raw) {
@@ -12856,6 +13262,13 @@ function adaptLarkChannel(channel) {
12856
13262
  { file: { source: content, fileName } },
12857
13263
  toLarkSendOptions(options)
12858
13264
  );
13265
+ },
13266
+ async sendImage(chatId, content, options) {
13267
+ await channel.send(
13268
+ chatId,
13269
+ { image: { source: content } },
13270
+ toLarkSendOptions(options)
13271
+ );
12859
13272
  }
12860
13273
  };
12861
13274
  return {
@@ -13300,7 +13713,7 @@ function safe(value, maxBytes) {
13300
13713
  }
13301
13714
 
13302
13715
  // src/session/projection-protocol.ts
13303
- import { randomUUID as randomUUID8 } from "crypto";
13716
+ import { randomUUID as randomUUID9 } from "crypto";
13304
13717
  var WebSessionProjectionSource = class {
13305
13718
  constructor(transport) {
13306
13719
  this.transport = transport;
@@ -13339,7 +13752,7 @@ var WebSessionProjectionSource = class {
13339
13752
  });
13340
13753
  return { events, hasMore: value.hasMore === true };
13341
13754
  }
13342
- async prompt(sessionId, text, rpcId = randomUUID8()) {
13755
+ async prompt(sessionId, text, rpcId = randomUUID9()) {
13343
13756
  responseValue(await this.transport.rpc("session.prompt", {
13344
13757
  sessionId,
13345
13758
  mode: "queue",
@@ -14012,7 +14425,7 @@ function wait(ms) {
14012
14425
  }
14013
14426
 
14014
14427
  // src/commands/session-projection.ts
14015
- import { randomUUID as randomUUID9 } from "crypto";
14428
+ import { randomUUID as randomUUID10 } from "crypto";
14016
14429
  var SessionProjectionController = class {
14017
14430
  constructor(deps) {
14018
14431
  this.deps = deps;
@@ -14152,7 +14565,7 @@ workspace: \`${workspaceCwd}\``
14152
14565
  if (owner && owner.scope !== input.identity.scope && !this.deps.access.isAdmin(input.identity.actorId)) {
14153
14566
  throw new Error("\u8BE5 session \u5DF2\u7ED1\u5B9A\u5176\u4ED6\u98DE\u4E66 scope\uFF1B\u72EC\u5360\u8FC1\u79FB\u4EC5 profile \u7BA1\u7406\u5458\u53EF\u786E\u8BA4");
14154
14567
  }
14155
- const nonce = randomUUID9();
14568
+ const nonce = randomUUID10();
14156
14569
  const expiresAt = Date.now() + (this.deps.confirmationTtlMs ?? 10 * 6e4);
14157
14570
  const timer = setTimeout(() => this.pending.delete(nonce), expiresAt - Date.now());
14158
14571
  timer.unref?.();
@@ -15177,13 +15590,19 @@ function attachRunCardAnchors(channel, anchors) {
15177
15590
  const result = await channel.sendFile(chatId, fileName, content, options);
15178
15591
  notify(chatId);
15179
15592
  return result;
15593
+ },
15594
+ async sendImage(chatId, content, options) {
15595
+ if (typeof channel.sendImage !== "function") return void 0;
15596
+ const result = await channel.sendImage(chatId, content, options);
15597
+ notify(chatId);
15598
+ return result;
15180
15599
  }
15181
15600
  };
15182
15601
  }
15183
15602
 
15184
15603
  // src/notify/server.ts
15185
15604
  import { createServer } from "http";
15186
- import { randomBytes as randomBytes2 } from "crypto";
15605
+ import { randomBytes as randomBytes3 } from "crypto";
15187
15606
  var NotifyServer = class {
15188
15607
  server;
15189
15608
  token;
@@ -15470,7 +15889,7 @@ function readBody(req) {
15470
15889
  });
15471
15890
  }
15472
15891
  function generateNotifyToken() {
15473
- return `dsh-lark-${randomBytes2(18).toString("hex")}`;
15892
+ return `dsh-lark-${randomBytes3(18).toString("hex")}`;
15474
15893
  }
15475
15894
 
15476
15895
  // src/notify/ask-handler.ts
@@ -15662,7 +16081,7 @@ async function settleCancelledCard(deps, chatId, cardMessageId, options) {
15662
16081
  }
15663
16082
 
15664
16083
  // src/notify/approval-handler.ts
15665
- import { randomUUID as randomUUID10 } from "crypto";
16084
+ import { randomUUID as randomUUID11 } from "crypto";
15666
16085
  init_tool_policy();
15667
16086
  function buildApprovalHandler(deps) {
15668
16087
  return async (payload, signal) => {
@@ -15705,7 +16124,7 @@ ${policyDenialText(denial)}
15705
16124
  return { ok: true, outcome: "rejected", denial };
15706
16125
  }
15707
16126
  if (payload.lowRisk) return { ok: true, outcome: "allowed-once" };
15708
- const id = `approval-${randomUUID10().replaceAll("-", "")}`;
16127
+ const id = `approval-${randomUUID11().replaceAll("-", "")}`;
15709
16128
  const request = {
15710
16129
  id,
15711
16130
  ...payload.callId === void 0 ? {} : { callId: payload.callId },
@@ -15949,21 +16368,6 @@ async function postJson(url, body3) {
15949
16368
  }
15950
16369
  }
15951
16370
 
15952
- // src/notify/sinks/text.ts
15953
- function renderSinkText(message) {
15954
- const zhLine = `${message.title.zh}\uFF08scope \`${message.scope}\`\uFF09${message.detail ? `\uFF1A${message.detail}` : ""}`;
15955
- const enLine = `${message.title.en} (scope \`${message.scope}\`)${message.detail ? `: ${message.detail}` : ""}`;
15956
- return `${stripMarkdown(zhLine)}
15957
-
15958
- ${stripMarkdown(enLine)}`;
15959
- }
15960
- function escapeHtml(value) {
15961
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
15962
- }
15963
- function stripMarkdown(value) {
15964
- return value.replaceAll("`", "").replaceAll("**", "").replaceAll("*", "");
15965
- }
15966
-
15967
16371
  // src/notify/sinks/telegram.ts
15968
16372
  var TelegramSink = class {
15969
16373
  constructor(baseUrl = "https://api.telegram.org") {
@@ -16038,9 +16442,72 @@ var WeComSink = class {
16038
16442
  }
16039
16443
  };
16040
16444
 
16445
+ // src/notify/sinks/qq.ts
16446
+ var QqSink = class {
16447
+ constructor(apiBase = "https://api.sgroup.qq.com", fetchAccessToken = getAppAccessToken) {
16448
+ this.apiBase = apiBase;
16449
+ this.fetchAccessToken = fetchAccessToken;
16450
+ }
16451
+ apiBase;
16452
+ fetchAccessToken;
16453
+ type = "qq";
16454
+ async send(channel, message) {
16455
+ const [appId, clientSecret] = splitAppCredential(channel.secret);
16456
+ const target = channel.destination;
16457
+ if (!appId || !clientSecret || !target) {
16458
+ log.warn("sink:qq", "missing-credential", { channel: channel.id });
16459
+ return false;
16460
+ }
16461
+ const isC2c = target.startsWith("user:");
16462
+ const openId = isC2c ? target.slice("user:".length) : target;
16463
+ const url = `${this.apiBase}/v2/${isC2c ? "users" : "groups"}/${encodeURIComponent(openId)}/messages`;
16464
+ try {
16465
+ const token = await this.fetchAccessToken(appId, clientSecret);
16466
+ const controller = new AbortController();
16467
+ const timer = setTimeout(() => controller.abort(), 1e4);
16468
+ timer.unref?.();
16469
+ const response = await fetch(url, {
16470
+ method: "POST",
16471
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
16472
+ body: JSON.stringify({ content: renderSinkText(message), msg_type: 0 }),
16473
+ signal: controller.signal
16474
+ });
16475
+ clearTimeout(timer);
16476
+ const payload = await response.json().catch(() => ({}));
16477
+ if (!response.ok || payload.code !== void 0 && payload.code !== 0) {
16478
+ log.warn("sink:qq", "send-failed", {
16479
+ channel: channel.id,
16480
+ error: payload.message ?? `http ${response.status}`
16481
+ });
16482
+ return false;
16483
+ }
16484
+ return true;
16485
+ } catch (error) {
16486
+ log.warn("sink:qq", "send-error", { channel: channel.id, error });
16487
+ return false;
16488
+ }
16489
+ }
16490
+ };
16491
+ function splitAppCredential(value) {
16492
+ const sep3 = value.indexOf(":");
16493
+ if (sep3 === -1) return [value, ""];
16494
+ return [value.slice(0, sep3), value.slice(sep3 + 1)];
16495
+ }
16496
+ async function getAppAccessToken(appId, clientSecret) {
16497
+ const response = await postJson(
16498
+ "https://bots.qq.com/app/getAppAccessToken",
16499
+ JSON.stringify({ appId, clientSecret })
16500
+ );
16501
+ const payload = await response.json();
16502
+ if (!response.ok || !payload.access_token) {
16503
+ throw new Error(payload.message ?? `access token http ${response.status}`);
16504
+ }
16505
+ return payload.access_token;
16506
+ }
16507
+
16041
16508
  // src/notify/sinks/registry.ts
16042
16509
  var OutboundSinkRegistry = class {
16043
- constructor(store, sinks = [new TelegramSink(), new WeComSink()]) {
16510
+ constructor(store, sinks = [new TelegramSink(), new WeComSink(), new WeChatIlinkSink(), new QqSink()]) {
16044
16511
  this.store = store;
16045
16512
  this.sinks = sinks;
16046
16513
  }
@@ -16665,7 +17132,7 @@ function normalizeContextSnapshot(value) {
16665
17132
 
16666
17133
  // src/session/projection-store.ts
16667
17134
  import { readFile as readFile26 } from "fs/promises";
16668
- import { randomUUID as randomUUID11 } from "crypto";
17135
+ import { randomUUID as randomUUID12 } from "crypto";
16669
17136
  var MAX_RECENT_MESSAGES = 64;
16670
17137
  var MAX_PROMPT_CORRELATIONS = 64;
16671
17138
  var PROMPT_CORRELATION_TTL_MS = 24 * 60 * 6e4;
@@ -16732,7 +17199,7 @@ var SessionProjectionStore = class {
16732
17199
  recentMessages: currentOwner?.scope === input.scope && currentOwner.workspaceCwd === input.workspaceCwd ? structuredClone(currentOwner.recentMessages) : [],
16733
17200
  promptCorrelations: structuredClone(currentOwner?.promptCorrelations ?? []),
16734
17201
  boundAt: (/* @__PURE__ */ new Date()).toISOString(),
16735
- generationId: randomUUID11()
17202
+ generationId: randomUUID12()
16736
17203
  };
16737
17204
  this.data.bindings[targetKey] = binding;
16738
17205
  return {
@@ -16889,7 +17356,7 @@ function normalizeCorrelation(value) {
16889
17356
 
16890
17357
  // src/session/archive.ts
16891
17358
  import { execFile as execFile2 } from "child_process";
16892
- import { randomBytes as randomBytes3 } from "crypto";
17359
+ import { randomBytes as randomBytes4 } from "crypto";
16893
17360
  import { access, mkdir as mkdir17, readdir as readdir3, readFile as readFile27, unlink, writeFile as writeFile8 } from "fs/promises";
16894
17361
  import { join as join24 } from "path";
16895
17362
  import { promisify } from "util";
@@ -16900,7 +17367,7 @@ function archiveScopeSlug(scope) {
16900
17367
  }
16901
17368
  function timestampId() {
16902
17369
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
16903
- return `${stamp}-${randomBytes3(3).toString("hex")}`;
17370
+ return `${stamp}-${randomBytes4(3).toString("hex")}`;
16904
17371
  }
16905
17372
  function renderMarkdown(input, record) {
16906
17373
  const lines = [
@@ -17214,7 +17681,7 @@ async function safeRemove(path) {
17214
17681
 
17215
17682
  // src/workspace/git-worktree.ts
17216
17683
  import { execFile as execFile3 } from "child_process";
17217
- import { createHash as createHash2, randomBytes as randomBytes4 } from "crypto";
17684
+ import { createHash as createHash2, randomBytes as randomBytes5 } from "crypto";
17218
17685
  import { access as access2, copyFile, mkdir as mkdir18, realpath as realpath2 } from "fs/promises";
17219
17686
  import { dirname as dirname16, join as join25 } from "path";
17220
17687
  import { promisify as promisify2 } from "util";
@@ -17275,7 +17742,7 @@ var GitWorktreeManager = class {
17275
17742
  return { cwd: target, created: false };
17276
17743
  }
17277
17744
  }
17278
- const branch = `dsh-lark/${slug}-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`;
17745
+ const branch = `dsh-lark/${slug}-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`;
17279
17746
  await mkdir18(this.options.worktreesRoot, { recursive: true });
17280
17747
  await this.runGit(["worktree", "add", "-b", branch, target, "HEAD"], base);
17281
17748
  await this.ensureProjectRules(base, target);
@@ -18259,7 +18726,7 @@ function projectDiagnosticLogs(lines) {
18259
18726
  }
18260
18727
 
18261
18728
  // src/secret/registry.ts
18262
- import { randomUUID as randomUUID12 } from "crypto";
18729
+ import { randomUUID as randomUUID13 } from "crypto";
18263
18730
  var SecretRequestRegistry = class {
18264
18731
  constructor(writer, options = {}) {
18265
18732
  this.writer = writer;
@@ -18272,7 +18739,7 @@ var SecretRequestRegistry = class {
18272
18739
  now;
18273
18740
  register(input) {
18274
18741
  this.writer.validate(input.target, input.reference);
18275
- const id = `secret-${randomUUID12().replaceAll("-", "")}`;
18742
+ const id = `secret-${randomUUID13().replaceAll("-", "")}`;
18276
18743
  let resolve6;
18277
18744
  const promise = new Promise((settle) => {
18278
18745
  resolve6 = settle;
@@ -18422,7 +18889,7 @@ function buildSecretHandler(deps) {
18422
18889
  init_own_package();
18423
18890
 
18424
18891
  // src/guardian/update-handoff.ts
18425
- import { createHash as createHash3, randomUUID as randomUUID13 } from "crypto";
18892
+ import { createHash as createHash3, randomUUID as randomUUID14 } from "crypto";
18426
18893
  import { spawn as spawn7 } from "child_process";
18427
18894
  import { chmod, mkdir as mkdir21, readFile as readFile32, rm as rm10 } from "fs/promises";
18428
18895
  import { dirname as dirname19, join as join26 } from "path";
@@ -18596,7 +19063,7 @@ var GuardianUpdateHandoff = class {
18596
19063
  this.options = options;
18597
19064
  this.launch = options.launch ?? defaultLaunch;
18598
19065
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
18599
- this.id = options.id ?? randomUUID13;
19066
+ this.id = options.id ?? randomUUID14;
18600
19067
  }
18601
19068
  options;
18602
19069
  launch;
@@ -18712,14 +19179,14 @@ var GuardianUpdateHandoff = class {
18712
19179
  };
18713
19180
 
18714
19181
  // src/upgrade/channel-update.ts
18715
- import { randomUUID as randomUUID14 } from "crypto";
19182
+ import { randomUUID as randomUUID15 } from "crypto";
18716
19183
  var OFFER_TTL_MS = 10 * 6e4;
18717
19184
  var ChannelUpdateController = class {
18718
19185
  constructor(options) {
18719
19186
  this.options = options;
18720
19187
  this.current = options.current ?? currentVersion();
18721
19188
  this.probe = options.probe ?? (() => latestVersion({ cacheMs: 0 }));
18722
- this.id = options.id ?? randomUUID14;
19189
+ this.id = options.id ?? randomUUID15;
18723
19190
  this.now = options.now ?? Date.now;
18724
19191
  this.ttl = options.offerTtlMs ?? OFFER_TTL_MS;
18725
19192
  }
@@ -19078,7 +19545,7 @@ async function startBridgeEngine(options) {
19078
19545
  ledgerClaimed = await claimJobDispatch({
19079
19546
  jobs,
19080
19547
  messageIds: ledgerMessageIds,
19081
- runId: `dispatch-${randomUUID15()}`,
19548
+ runId: `dispatch-${randomUUID16()}`,
19082
19549
  first,
19083
19550
  channel: streaming
19084
19551
  });
@@ -20193,7 +20660,7 @@ ${changeLines.join("\n")}
20193
20660
  import { join as join33 } from "path";
20194
20661
 
20195
20662
  // src/guardian/service.ts
20196
- import { randomUUID as randomUUID16 } from "crypto";
20663
+ import { randomUUID as randomUUID17 } from "crypto";
20197
20664
  import { readFileSync as readFileSync6 } from "fs";
20198
20665
  import { homedir as homedir21 } from "os";
20199
20666
  import { join as join32 } from "path";
@@ -21040,7 +21507,7 @@ var GuardianService = class {
21040
21507
  }
21041
21508
  const transcript = this.transcripts.get(scope) ?? [];
21042
21509
  const prompt = buildSafePrompt(transcript, msg.content);
21043
- const runId = randomUUID16();
21510
+ const runId = randomUUID17();
21044
21511
  const density = this.options.safeDensity;
21045
21512
  const timeoutMs = this.options.safeTimeoutMs;
21046
21513
  const now = Date.now();
@@ -21741,7 +22208,7 @@ function waitForExit(child) {
21741
22208
  child.once("error", done);
21742
22209
  });
21743
22210
  }
21744
- function sleep3(ms) {
22211
+ function sleep4(ms) {
21745
22212
  return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
21746
22213
  }
21747
22214
  async function runSupervise(options, deps = {}) {
@@ -21811,7 +22278,7 @@ async function runSupervise(options, deps = {}) {
21811
22278
  restarts += 1;
21812
22279
  await writeStatus("restarting", void 0, restarts);
21813
22280
  const backoff = computeBackoffMs(restarts);
21814
- await sleep3(backoff);
22281
+ await sleep4(backoff);
21815
22282
  }
21816
22283
  } finally {
21817
22284
  if (forceStopTimer) clearTimeout(forceStopTimer);