dsh-lark-agent 0.3.1 → 0.3.3

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/README.md CHANGED
@@ -30,8 +30,6 @@ Before you start:
30
30
 
31
31
  In the sidebar, open **插件 → 添加插件** (Plugins → Add plugin), enter `dsh-lark-agent`, click **安装** (Install), then **立即启用** (Enable now). Restart if the app asks.
32
32
 
33
- <img src="https://raw.githubusercontent.com/Nirvana-Jie/dsh-lark-agent/main/assets/readme/desktop-install-02.jpg" width="720" alt="The Add plugin dialog in the DeepSeek Harness desktop app">
34
-
35
33
  ### Install with the `dsh` CLI
36
34
 
37
35
  [`dsh plugin`](https://www.npmjs.com/package/@deepseek-ai/dsh) runs pnpm, so pnpm must be on your PATH:
@@ -53,7 +51,7 @@ Then restart `dsh web`.
53
51
 
54
52
  ### Update
55
53
 
56
- When a new version is out, the Feishu settings page shows a notice with these steps (in the desktop app it also opens the plugin list for you).
54
+ When a new version has been out for a day, the Feishu settings page shows a notice with these steps (in the desktop app it also opens the plugin list for you). Until then the desktop app and `dsh plugin` still install the previous version, because pnpm 11 skips releases less than 24 hours old. If an install picks an older version that your DeepSeek Harness rejects as incompatible, see [Troubleshooting](https://github.com/Nirvana-Jie/dsh-lark-agent/blob/main/DEVELOPMENT.md#排障).
57
55
 
58
56
  1. On the Feishu settings page, click **暂停连接** (pause) and back up as described in [Upgrade](https://github.com/Nirvana-Jie/dsh-lark-agent/blob/main/DEVELOPMENT.md#升级).
59
57
  2. Desktop app: the Add plugin dialog says third-party plugins must be uninstalled before a new version is installed, so uninstall `dsh-lark-agent` on the Plugins page and add it again (this in-app upgrade hasn't been tested end to end yet; an earlier source build is listed as `dsh-larkbot`). CLI: run `dsh plugin --profile web update dsh-lark-agent@latest`, then restart `dsh web`.
@@ -10,6 +10,12 @@ export declare const BOT_MENU_COMMANDS: Readonly<{
10
10
  }>;
11
11
  export type BotMenuCommand = (typeof BOT_MENU_COMMANDS)[keyof typeof BOT_MENU_COMMANDS];
12
12
  export declare function botMenuCommand(eventKey: unknown): BotMenuCommand | undefined;
13
+ /** A menu entry as the owner adds it in the developer console. */
14
+ export interface BotMenuEntry {
15
+ label: string;
16
+ eventKey: string;
17
+ parent?: string;
18
+ }
13
19
  export interface BotMenuNode {
14
20
  menu_id: string;
15
21
  parent_menu_id?: string;
@@ -32,6 +38,12 @@ export declare function buildBotMenuAbility(): {
32
38
  bot_menus: BotMenuNode[];
33
39
  };
34
40
  };
41
+ /**
42
+ * The entries a published menu lacks, by label. A menu configured by hand
43
+ * before a release added an entry stays without it; none is reported when
44
+ * no dsh entry was configured at all.
45
+ */
46
+ export declare function missingBotMenuEntries(eventKeys: readonly string[]): BotMenuEntry[];
35
47
  /** Pure review/export plan: this module never invokes an API or changes an app. */
36
48
  export declare function buildBotMenuPlan(appId: string): {
37
49
  scope: string;
package/dist/bot-menu.js CHANGED
@@ -29,6 +29,20 @@ export function buildBotMenuAbility() {
29
29
  { menu_id: 'dsh_permission', parent_menu_id: 'dsh_settings', sort: 2, default_name: '权限模式', menu_content_type: 2, event_key: 'dsh.permission' },
30
30
  ] } };
31
31
  }
32
+ /**
33
+ * The entries a published menu lacks, by label. A menu configured by hand
34
+ * before a release added an entry stays without it; none is reported when
35
+ * no dsh entry was configured at all.
36
+ */
37
+ export function missingBotMenuEntries(eventKeys) {
38
+ const nodes = buildBotMenuAbility().bot.bot_menus, entries = nodes.filter(node => node.event_key);
39
+ if (!entries.some(node => eventKeys.includes(node.event_key)))
40
+ return [];
41
+ return entries.filter(node => !eventKeys.includes(node.event_key)).map(node => {
42
+ const parent = nodes.find(candidate => candidate.menu_id === node.parent_menu_id)?.default_name;
43
+ return { label: node.default_name, eventKey: node.event_key, ...(parent ? { parent } : {}) };
44
+ });
45
+ }
32
46
  /** Pure review/export plan: this module never invokes an API or changes an app. */
33
47
  export function buildBotMenuPlan(appId) {
34
48
  if (typeof appId !== 'string' || !/^cli_[A-Za-z0-9_-]{1,128}$/.test(appId))
package/dist/bridge.js CHANGED
@@ -433,9 +433,11 @@ export class BotBridge {
433
433
  return;
434
434
  const controller = new AbortController();
435
435
  lane.controller = controller;
436
- let timedOut = false;
437
- const timer = setTimeout(() => { timedOut = true; controller.abort(); }, this.config.turnTimeoutMs);
438
436
  const harness = message.chatType === 'p2p' && lane.user.directMode === 'harness';
437
+ let timedOut = false;
438
+ // Like the App, a turn runs until it ends or someone sends /stop. Only a configured cap bounds a restricted turn.
439
+ const deadline = harness ? undefined : this.config.turnTimeoutMs;
440
+ const timer = deadline === undefined ? undefined : setTimeout(() => { timedOut = true; controller.abort(); }, deadline);
439
441
  let live;
440
442
  // Once a live card exists it carries this turn's whole outcome; nothing else is sent as text.
441
443
  const conclude = async (final) => {
package/dist/client.cjs CHANGED
@@ -111,6 +111,15 @@ function parseSnapshot(value) {
111
111
  snapshot.documentSetup.qr = { url: officialUrl(qr.url), image, expiresAt: timestamp(qr.expiresAt) };
112
112
  }
113
113
  }
114
+ if (data.menu !== void 0) {
115
+ const menu = object(data.menu);
116
+ if (!Array.isArray(menu.missing) || !menu.missing.length || menu.missing.length > 10) throw new Error("invalid menu");
117
+ snapshot.menu = { url: officialUrl(menu.url), missing: menu.missing.map((value2) => {
118
+ const entry = object(value2);
119
+ if (typeof entry.eventKey !== "string" || !/^dsh\.[a-z]{1,30}$/.test(entry.eventKey)) throw new Error("invalid menu");
120
+ return { label: text(entry.label, 60), eventKey: entry.eventKey, ...entry.parent === void 0 ? {} : { parent: text(entry.parent, 60) } };
121
+ }) };
122
+ }
114
123
  if (data.update !== void 0) {
115
124
  const update = object(data.update);
116
125
  const version = (value2) => {
@@ -330,6 +339,18 @@ function Steps({ active }) {
330
339
  label
331
340
  ] }, label)) });
332
341
  }
342
+ function MenuNotice({ menu, phase }) {
343
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "lark-update", role: "status", children: [
344
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "\u98DE\u4E66\u5E95\u90E8\u83DC\u5355\u7F3A\u5C11\u5165\u53E3" }),
345
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: menu.missing.map((entry) => `${entry.parent ? `${entry.parent} \u2192 ` : ""}${entry.label}\uFF08\u4E8B\u4EF6\u6807\u8BC6 ${entry.eventKey}\uFF09`).join("\u3001") }),
346
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ol", { className: "lark-instructions", children: [
347
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "\u5728\u98DE\u4E66\u5F00\u53D1\u8005\u540E\u53F0\u6253\u5F00\u300C\u673A\u5668\u4EBA\u81EA\u5B9A\u4E49\u83DC\u5355\u300D\u8865\u4E0A\uFF0C\u54CD\u5E94\u52A8\u4F5C\u9009\u300C\u63A8\u9001\u4E8B\u4EF6\u300D\uFF0C\u4E8B\u4EF6\u6807\u8BC6\u9010\u5B57\u586B\u5199\u3002" }),
348
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "\u521B\u5EFA\u7248\u672C\u5E76\u53D1\u5E03\uFF0C\u5BA2\u6237\u7AEF\u7EA6 5 \u5206\u949F\u540E\u66F4\u65B0\u3002" }),
349
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: phase === "paused" ? "\u56DE\u5230\u8FD9\u91CC\u70B9\u300C\u6062\u590D\u8FDE\u63A5\u300D\u3002" : "\u56DE\u5230\u8FD9\u91CC\u70B9\u300C\u6682\u505C\u8FDE\u63A5\u300D\uFF0C\u518D\u70B9\u300C\u6062\u590D\u8FDE\u63A5\u300D\u3002" })
350
+ ] }),
351
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "lark-actions", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(External, { url: menu.url, children: "\u6253\u5F00\u98DE\u4E66\u540E\u53F0" }) })
352
+ ] });
353
+ }
333
354
  function UpdateNotice({ update, host, phase }) {
334
355
  const [open, setOpen] = (0, import_react2.useState)(false);
335
356
  const [status, setStatus] = (0, import_react2.useState)("");
@@ -594,6 +615,7 @@ function ConnectionCard({ api, host }) {
594
615
  ] }),
595
616
  !snapshot.capabilities.documents && !snapshot.documentSetup && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "lark-secondary", disabled, onClick: () => void mutate("enable-documents"), children: "\u8865\u5145\u9ED8\u8BA4\u6743\u9650" })
596
617
  ] }),
618
+ snapshot.menu && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MenuNotice, { menu: snapshot.menu, phase }),
597
619
  snapshot.documentSetup && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "lark-notice", role: "status", children: [
598
620
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: snapshot.documentSetup.message }),
599
621
  snapshot.documentSetup.phase === "preparing" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "lark-spinner", "aria-label": "\u6B63\u5728\u51C6\u5907\u6388\u6743" }),
@@ -671,7 +693,7 @@ function ConnectionCard({ api, host }) {
671
693
  // src/client/styles.ts
672
694
  var css = `
673
695
  .dsh-lark-settings{--lark-ink:#263247;--lark-muted:#738096;--lark-line:#e4eaf2;--lark-blue:#4268df;--lark-surface:#fff;--lark-soft:#f5f8fd;font:14px/1.65 -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;color:var(--lark-ink);max-width:720px;margin:0 auto;padding:8px 0 20px;box-sizing:border-box}
674
- .dsh-lark-settings *{box-sizing:border-box}.dsh-lark-settings button,.dsh-lark-settings input{font:inherit}.dsh-lark-settings button,.dsh-lark-settings summary{cursor:pointer}.dsh-lark-settings button:disabled{opacity:.5;cursor:not-allowed}.dsh-lark-settings button:focus-visible,.dsh-lark-settings a:focus-visible,.dsh-lark-settings input:focus-visible,.dsh-lark-settings summary:focus-visible{outline:3px solid #a1b8ff;outline-offset:3px}.dsh-lark-settings a{overflow-wrap:anywhere}.lark-heading{margin:0 0 22px}.lark-heading h2{font-size:23px;font-weight:650;margin:0 0 5px;letter-spacing:-.4px}.lark-heading p,.lark-card-header p{color:var(--lark-muted);font-size:13px;margin:0}.lark-card{border:1px solid var(--lark-line);border-radius:15px;background:var(--lark-surface);overflow:hidden;box-shadow:0 2px 5px #27385504}.lark-card-header{display:flex;align-items:center;gap:12px;padding:19px 23px;border-bottom:1px solid var(--lark-line)}.lark-plugin-icon{width:43px;height:43px;object-fit:contain;flex-shrink:0}.lark-card-header h2{font-size:15px;font-weight:650;line-height:1.6;margin:0}.lark-card-header p{font-size:12px}.lark-status{margin-left:auto;display:flex;align-items:center;gap:6px;font-size:11px;color:var(--lark-muted);white-space:nowrap}.lark-status i{width:6px;height:6px;border-radius:50%;background:#9da9bc}.lark-status.online{color:#248470}.lark-status.online i{background:#248470}.lark-body{padding:28px;position:relative}.lark-body h3{font-size:20px;font-weight:650;letter-spacing:-.25px;margin:0 0 9px;line-height:1.5}.lark-copy{color:var(--lark-muted);font-size:13px;margin:0 0 20px}.lark-centered{text-align:center;line-height:1.9}.lark-body .lark-intro-title{text-align:center;font-size:21px}.lark-brands{display:flex;justify-content:center;align-items:center;gap:16px;margin:5px 0 25px}.lark-brand{width:53px;height:53px;display:grid;place-items:center;border-radius:14px;background:var(--lark-soft);border:1px solid var(--lark-line)}.lark-brand img{width:37px;height:37px;object-fit:contain}.lark-brand-line{width:78px;height:1px;background:#dbe3ee;position:relative}.lark-brand-line:after{content:"";position:absolute;left:calc(50% - 3px);top:-3px;width:7px;height:7px;border-radius:50%;background:#a9bbdc;border:2px solid var(--lark-surface)}.lark-brands-connected .lark-brand-line{background:#99d1c1}.lark-brands-connected .lark-brand-line:after{background:#248470}.lark-primary,.lark-secondary,.lark-quiet,.lark-danger{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:38px;padding:8px 16px;border-radius:8px;text-decoration:none;font-size:13px;font-weight:550;border:1px solid transparent;line-height:1.5;transition:background .14s}.lark-primary{background:var(--lark-blue);color:#fff}.lark-primary:hover{background:#3559ca}.lark-secondary{color:#485b79;background:var(--lark-surface);border-color:#dce3ed}.lark-secondary:hover{background:var(--lark-soft)}.lark-quiet{background:transparent;color:var(--lark-muted);font-weight:450}.lark-quiet:hover{background:var(--lark-soft)}.lark-danger{color:#ad4949;border-color:#e5bcbc;background:var(--lark-surface)}.lark-text-button{display:inline-flex;align-items:center;gap:7px;background:transparent;border:0;padding:5px 0;color:#5875a5;font-size:12px}.lark-entry{display:flex;flex-direction:column;align-items:center;gap:10px}.lark-entry .lark-primary{min-width:200px}.lark-actions{display:flex;gap:9px;flex-wrap:wrap;align-items:center;margin-top:23px}.lark-spread{justify-content:space-between}.lark-center{justify-content:center}.lark-footnote{color:var(--lark-muted);font-size:11px;text-align:center;margin:18px 0 0}.lark-left{text-align:left}.lark-bottom-note{text-align:center;color:var(--lark-muted);font-size:11px;margin:15px 0 0}.lark-steps{display:flex;list-style:none;gap:12px;margin:0 0 25px;padding:0;color:#8b98ad;font-size:11px}.lark-steps li{display:flex;gap:6px;align-items:center;white-space:nowrap}.lark-steps li+li:before{content:"";width:23px;height:1px;background:var(--lark-line);margin-right:5px}.lark-steps li span{width:18px;height:18px;display:grid;place-items:center;border-radius:50%;background:var(--lark-soft);font-size:10px}.lark-steps .active{color:var(--lark-blue);font-weight:600}.lark-steps .active span{background:#edf2ff}.lark-steps .done{color:#248470}.lark-steps .done span{background:#edf8f4}.lark-qr-layout{display:flex;align-items:center;gap:25px;margin:23px 0}.lark-qr{position:relative;width:166px;height:166px;padding:10px;background:white;border:1px solid var(--lark-line);border-radius:12px;flex-shrink:0}.lark-qr img{width:100%;height:100%;object-fit:contain}.lark-qr.expired img{opacity:.12}.lark-qr>span{position:absolute;inset:0;display:grid;place-items:center;color:#64758e;font-weight:600}.lark-qr-layout strong{font-weight:600;font-size:13px}.lark-qr-layout .lark-copy{margin:7px 0 10px}.lark-time{color:var(--lark-muted);font-size:11px;font-variant-numeric:tabular-nums}.lark-link-details{border-top:1px solid var(--lark-line);padding:13px 0 0;font-size:12px;color:var(--lark-muted)}.lark-link-details summary{margin-bottom:12px}.lark-raw-url{font-size:10px;line-height:1.65;overflow-wrap:anywhere;margin-top:10px;color:var(--lark-muted);user-select:all}.lark-notice{padding:13px 15px;background:#eef3fe;border-radius:9px;color:#526c95;font-size:12px;line-height:1.8}.lark-notice.warning{background:#fff7e9;color:#8b671e}.lark-state-icon{display:grid;place-items:center;width:40px;height:40px;border-radius:50%;background:#edf8f4;color:#248470;font-size:20px;margin:2px 0 18px}.lark-state-icon.warning{background:#fff7e9;color:#9b7530}.lark-checks{padding:0;list-style:none;margin:20px 0;font-size:13px;color:var(--lark-muted)}.lark-checks li{padding:10px 0;border-bottom:1px solid var(--lark-line)}.lark-checks li:before{content:"\xB7";margin-right:12px}.lark-pairing{background:var(--lark-soft);border:1px solid var(--lark-line);border-radius:11px;padding:17px 18px;display:flex;align-items:center;justify-content:space-between;gap:13px}.lark-pair-code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:21px;letter-spacing:.02em;color:#3d5a91;margin:4px 0 2px;overflow-wrap:anywhere;user-select:all}.lark-pairing.expired .lark-pair-code{opacity:.5}.lark-caption{font-size:11px;color:var(--lark-muted)}.lark-copy-status{min-height:18px;font-size:11px;color:#248470;margin:5px 0}.lark-instructions{padding-left:20px;color:#536784;font-size:13px}.lark-instructions li{padding:3px 0}.lark-owner{padding:17px 18px;background:var(--lark-soft);border:1px solid var(--lark-line);border-radius:10px;display:grid;gap:6px}.lark-owner strong{font-size:15px;font-weight:600;overflow-wrap:anywhere}.lark-connected-row{border-top:1px solid var(--lark-line);border-bottom:1px solid var(--lark-line);display:flex;justify-content:space-between;align-items:center;gap:16px;padding:16px 0;margin:26px 0 0}.lark-connected-row strong{font-size:12px;font-weight:600}.lark-connected-row p{font-size:11px;color:var(--lark-muted);margin:3px 0 0}.lark-advanced{font-size:12px;color:var(--lark-muted);padding-top:14px}.lark-advanced p{margin:9px 0 0;line-height:1.9}.lark-disconnect{border:1px solid #eacbcb;padding:14px;border-radius:10px;margin-top:16px;font-size:12px}.lark-disconnect p{margin:0}.lark-disconnect .lark-actions{margin-top:12px}.lark-profile-list{border:0;margin:20px 0 0;padding:0;display:grid;gap:10px;min-width:0}.lark-profile{display:flex;align-items:center;gap:12px;border:1px solid var(--lark-line);border-radius:10px;padding:15px;background:var(--lark-soft);cursor:pointer;overflow-wrap:anywhere}.lark-profile input{accent-color:var(--lark-blue)}.lark-error{display:flex;align-items:center;justify-content:space-between;gap:12px;background:#fff1f1;border:1px solid #f0d5d5;color:#9b4e4e;border-radius:9px;padding:11px 13px;font-size:12px;margin-bottom:21px}.lark-error .lark-text-button{flex-shrink:0;color:#9b4e4e}.lark-loading{padding:27px 0;text-align:center}.lark-loading .lark-spinner{margin:0 auto 17px;display:block}.lark-loading p{font-size:13px;color:var(--lark-muted);margin:0}.lark-loading-row{display:flex;align-items:center;gap:12px;color:var(--lark-muted);font-size:13px;margin:4px 0 15px}.lark-loading-row h3{color:var(--lark-ink);margin:0}.lark-spinner{display:inline-block;width:18px;height:18px;border:2px solid #dfe6f4;border-top-color:var(--lark-blue);border-radius:50%;animation:lark-spin .85s linear infinite;flex-shrink:0}.lark-pending{display:flex;align-items:center;gap:9px;margin-top:19px;font-size:11px;color:var(--lark-muted)}.lark-pending .lark-spinner{width:13px;height:13px}.lark-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.lark-update{border:1px solid #cfdcf8;background:#f4f7ff;border-radius:11px;padding:14px 16px;margin:0 0 22px}.lark-update-row{display:flex;align-items:center;justify-content:space-between;gap:14px}.lark-update strong{font-size:13px;font-weight:600}.lark-update p{font-size:12px;color:var(--lark-muted);margin:3px 0 0}.lark-update .lark-instructions{margin:12px 0 0}.lark-update .lark-actions{margin-top:12px}.lark-update .lark-copy-status{margin:6px 0 0}.lark-command{display:block;margin-top:6px;padding:8px 10px;border-radius:7px;background:var(--lark-surface);border:1px solid var(--lark-line);font:12px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere;user-select:all}
696
+ .dsh-lark-settings *{box-sizing:border-box}.dsh-lark-settings button,.dsh-lark-settings input{font:inherit}.dsh-lark-settings button,.dsh-lark-settings summary{cursor:pointer}.dsh-lark-settings button:disabled{opacity:.5;cursor:not-allowed}.dsh-lark-settings button:focus-visible,.dsh-lark-settings a:focus-visible,.dsh-lark-settings input:focus-visible,.dsh-lark-settings summary:focus-visible{outline:3px solid #a1b8ff;outline-offset:3px}.dsh-lark-settings a{overflow-wrap:anywhere}.lark-heading{margin:0 0 22px}.lark-heading h2{font-size:23px;font-weight:650;margin:0 0 5px;letter-spacing:-.4px}.lark-heading p,.lark-card-header p{color:var(--lark-muted);font-size:13px;margin:0}.lark-card{border:1px solid var(--lark-line);border-radius:15px;background:var(--lark-surface);overflow:hidden;box-shadow:0 2px 5px #27385504}.lark-card-header{display:flex;align-items:center;gap:12px;padding:19px 23px;border-bottom:1px solid var(--lark-line)}.lark-plugin-icon{width:43px;height:43px;object-fit:contain;flex-shrink:0}.lark-card-header h2{font-size:15px;font-weight:650;line-height:1.6;margin:0}.lark-card-header p{font-size:12px}.lark-status{margin-left:auto;display:flex;align-items:center;gap:6px;font-size:11px;color:var(--lark-muted);white-space:nowrap}.lark-status i{width:6px;height:6px;border-radius:50%;background:#9da9bc}.lark-status.online{color:#248470}.lark-status.online i{background:#248470}.lark-body{padding:28px;position:relative}.lark-body h3{font-size:20px;font-weight:650;letter-spacing:-.25px;margin:0 0 9px;line-height:1.5}.lark-copy{color:var(--lark-muted);font-size:13px;margin:0 0 20px}.lark-centered{text-align:center;line-height:1.9}.lark-body .lark-intro-title{text-align:center;font-size:21px}.lark-brands{display:flex;justify-content:center;align-items:center;gap:16px;margin:5px 0 25px}.lark-brand{width:53px;height:53px;display:grid;place-items:center;border-radius:14px;background:var(--lark-soft);border:1px solid var(--lark-line)}.lark-brand img{width:37px;height:37px;object-fit:contain}.lark-brand-line{width:78px;height:1px;background:#dbe3ee;position:relative}.lark-brand-line:after{content:"";position:absolute;left:calc(50% - 3px);top:-3px;width:7px;height:7px;border-radius:50%;background:#a9bbdc;border:2px solid var(--lark-surface)}.lark-brands-connected .lark-brand-line{background:#99d1c1}.lark-brands-connected .lark-brand-line:after{background:#248470}.lark-primary,.lark-secondary,.lark-quiet,.lark-danger{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:38px;padding:8px 16px;border-radius:8px;text-decoration:none;font-size:13px;font-weight:550;border:1px solid transparent;line-height:1.5;transition:background .14s}.lark-primary{background:var(--lark-blue);color:#fff}.lark-primary:hover{background:#3559ca}.lark-secondary{color:#485b79;background:var(--lark-surface);border-color:#dce3ed}.lark-secondary:hover{background:var(--lark-soft)}.lark-quiet{background:transparent;color:var(--lark-muted);font-weight:450}.lark-quiet:hover{background:var(--lark-soft)}.lark-danger{color:#ad4949;border-color:#e5bcbc;background:var(--lark-surface)}.lark-text-button{display:inline-flex;align-items:center;gap:7px;background:transparent;border:0;padding:5px 0;color:#5875a5;font-size:12px}.lark-entry{display:flex;flex-direction:column;align-items:center;gap:10px}.lark-entry .lark-primary{min-width:200px}.lark-actions{display:flex;gap:9px;flex-wrap:wrap;align-items:center;margin-top:23px}.lark-spread{justify-content:space-between}.lark-center{justify-content:center}.lark-footnote{color:var(--lark-muted);font-size:11px;text-align:center;margin:18px 0 0}.lark-left{text-align:left}.lark-bottom-note{text-align:center;color:var(--lark-muted);font-size:11px;margin:15px 0 0}.lark-steps{display:flex;list-style:none;gap:12px;margin:0 0 25px;padding:0;color:#8b98ad;font-size:11px}.lark-steps li{display:flex;gap:6px;align-items:center;white-space:nowrap}.lark-steps li+li:before{content:"";width:23px;height:1px;background:var(--lark-line);margin-right:5px}.lark-steps li span{width:18px;height:18px;display:grid;place-items:center;border-radius:50%;background:var(--lark-soft);font-size:10px}.lark-steps .active{color:var(--lark-blue);font-weight:600}.lark-steps .active span{background:#edf2ff}.lark-steps .done{color:#248470}.lark-steps .done span{background:#edf8f4}.lark-qr-layout{display:flex;align-items:center;gap:25px;margin:23px 0}.lark-qr{position:relative;width:166px;height:166px;padding:10px;background:white;border:1px solid var(--lark-line);border-radius:12px;flex-shrink:0}.lark-qr img{width:100%;height:100%;object-fit:contain}.lark-qr.expired img{opacity:.12}.lark-qr>span{position:absolute;inset:0;display:grid;place-items:center;color:#64758e;font-weight:600}.lark-qr-layout strong{font-weight:600;font-size:13px}.lark-qr-layout .lark-copy{margin:7px 0 10px}.lark-time{color:var(--lark-muted);font-size:11px;font-variant-numeric:tabular-nums}.lark-link-details{border-top:1px solid var(--lark-line);padding:13px 0 0;font-size:12px;color:var(--lark-muted)}.lark-link-details summary{margin-bottom:12px}.lark-raw-url{font-size:10px;line-height:1.65;overflow-wrap:anywhere;margin-top:10px;color:var(--lark-muted);user-select:all}.lark-notice{padding:13px 15px;background:#eef3fe;border-radius:9px;color:#526c95;font-size:12px;line-height:1.8}.lark-notice.warning{background:#fff7e9;color:#8b671e}.lark-state-icon{display:grid;place-items:center;width:40px;height:40px;border-radius:50%;background:#edf8f4;color:#248470;font-size:20px;margin:2px 0 18px}.lark-state-icon.warning{background:#fff7e9;color:#9b7530}.lark-checks{padding:0;list-style:none;margin:20px 0;font-size:13px;color:var(--lark-muted)}.lark-checks li{padding:10px 0;border-bottom:1px solid var(--lark-line)}.lark-checks li:before{content:"\xB7";margin-right:12px}.lark-pairing{background:var(--lark-soft);border:1px solid var(--lark-line);border-radius:11px;padding:17px 18px;display:flex;align-items:center;justify-content:space-between;gap:13px}.lark-pair-code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:21px;letter-spacing:.02em;color:#3d5a91;margin:4px 0 2px;overflow-wrap:anywhere;user-select:all}.lark-pairing.expired .lark-pair-code{opacity:.5}.lark-caption{font-size:11px;color:var(--lark-muted)}.lark-copy-status{min-height:18px;font-size:11px;color:#248470;margin:5px 0}.lark-instructions{padding-left:20px;color:#536784;font-size:13px}.lark-instructions li{padding:3px 0}.lark-owner{padding:17px 18px;background:var(--lark-soft);border:1px solid var(--lark-line);border-radius:10px;display:grid;gap:6px}.lark-owner strong{font-size:15px;font-weight:600;overflow-wrap:anywhere}.lark-connected-row{border-top:1px solid var(--lark-line);border-bottom:1px solid var(--lark-line);display:flex;justify-content:space-between;align-items:center;gap:16px;padding:16px 0;margin:26px 0 0}.lark-connected-row strong{font-size:12px;font-weight:600}.lark-connected-row p{font-size:11px;color:var(--lark-muted);margin:3px 0 0}.lark-advanced{font-size:12px;color:var(--lark-muted);padding-top:14px}.lark-advanced p{margin:9px 0 0;line-height:1.9}.lark-disconnect{border:1px solid #eacbcb;padding:14px;border-radius:10px;margin-top:16px;font-size:12px}.lark-disconnect p{margin:0}.lark-disconnect .lark-actions{margin-top:12px}.lark-profile-list{border:0;margin:20px 0 0;padding:0;display:grid;gap:10px;min-width:0}.lark-profile{display:flex;align-items:center;gap:12px;border:1px solid var(--lark-line);border-radius:10px;padding:15px;background:var(--lark-soft);cursor:pointer;overflow-wrap:anywhere}.lark-profile input{accent-color:var(--lark-blue)}.lark-error{display:flex;align-items:center;justify-content:space-between;gap:12px;background:#fff1f1;border:1px solid #f0d5d5;color:#9b4e4e;border-radius:9px;padding:11px 13px;font-size:12px;margin-bottom:21px}.lark-error .lark-text-button{flex-shrink:0;color:#9b4e4e}.lark-loading{padding:27px 0;text-align:center}.lark-loading .lark-spinner{margin:0 auto 17px;display:block}.lark-loading p{font-size:13px;color:var(--lark-muted);margin:0}.lark-loading-row{display:flex;align-items:center;gap:12px;color:var(--lark-muted);font-size:13px;margin:4px 0 15px}.lark-loading-row h3{color:var(--lark-ink);margin:0}.lark-spinner{display:inline-block;width:18px;height:18px;border:2px solid #dfe6f4;border-top-color:var(--lark-blue);border-radius:50%;animation:lark-spin .85s linear infinite;flex-shrink:0}.lark-pending{display:flex;align-items:center;gap:9px;margin-top:19px;font-size:11px;color:var(--lark-muted)}.lark-pending .lark-spinner{width:13px;height:13px}.lark-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.lark-update{border:1px solid #cfdcf8;background:#f4f7ff;border-radius:11px;padding:14px 16px;margin:0 0 22px}.lark-connected-row+.lark-update{margin:16px 0 0}.lark-update-row{display:flex;align-items:center;justify-content:space-between;gap:14px}.lark-update strong{font-size:13px;font-weight:600}.lark-update p{font-size:12px;color:var(--lark-muted);margin:3px 0 0}.lark-update .lark-instructions{margin:12px 0 0}.lark-update .lark-actions{margin-top:12px}.lark-update .lark-copy-status{margin:6px 0 0}.lark-command{display:block;margin-top:6px;padding:8px 10px;border-radius:7px;background:var(--lark-surface);border:1px solid var(--lark-line);font:12px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere;user-select:all}
675
697
  @keyframes lark-spin{to{transform:rotate(360deg)}}
676
698
  @media(prefers-color-scheme:dark){.dsh-lark-settings{--lark-ink:#e0e7f1;--lark-muted:#96a6bd;--lark-line:#364357;--lark-surface:#202a39;--lark-soft:#283448;--lark-blue:#5077ec}.lark-secondary{color:#c6d5ed;border-color:#46536b}.lark-pair-code{color:#b9ceff}.lark-instructions{color:#b0c2de}.lark-notice{background:#273d5a;color:#c2d3ec}.lark-notice.warning,.lark-state-icon.warning{background:#44391f;color:#e4c886}.lark-error{background:#492e32;border-color:#684047;color:#edbec2}.lark-error .lark-text-button{color:#edbec2}.lark-steps .active span{background:#293e6b}.lark-steps .done span,.lark-state-icon{background:#23483f}.lark-status.online,.lark-steps .done{color:#78c5b0}.lark-update{background:#24324a;border-color:#3a4d70}}
677
699
  @media(max-width:520px){.lark-heading{margin-bottom:17px}.lark-card-header{padding:16px;gap:10px}.lark-card-header p{display:none}.lark-plugin-icon{width:37px;height:37px}.lark-body{padding:21px 18px}.lark-steps{gap:7px}.lark-steps li+li:before{width:12px;margin-right:2px}.lark-qr-layout{gap:17px;align-items:flex-start}.lark-qr{width:138px;height:138px}.lark-qr-layout strong{font-size:12px}.lark-qr-layout .lark-copy{font-size:12px}.lark-pairing{align-items:flex-start;flex-direction:column}.lark-pair-code{font-size:19px}.lark-body h3{font-size:19px}.lark-connected-row{gap:10px}.lark-actions{gap:8px}.lark-primary,.lark-secondary,.lark-quiet,.lark-danger{padding:8px 12px}.lark-copy{font-size:12px}.lark-update-row{flex-direction:column;align-items:flex-start}}
package/dist/config.d.ts CHANGED
@@ -19,7 +19,8 @@ export interface BotConfig {
19
19
  provider?: string;
20
20
  model?: string;
21
21
  users: UserPolicy[];
22
- turnTimeoutMs: number;
22
+ /** Optional cap on a restricted turn; unset, a turn runs until done or /stop. */
23
+ turnTimeoutMs?: number;
23
24
  idleTimeoutMs: number;
24
25
  maxSessions: number;
25
26
  maxSessionsPerUser: number;
package/dist/config.js CHANGED
@@ -73,8 +73,8 @@ export function parseConfig(value, baseDir = process.cwd()) {
73
73
  provider: data.provider === undefined ? undefined : string(data.provider, 'provider'),
74
74
  model: data.model === undefined ? undefined : string(data.model, 'model'),
75
75
  users,
76
- // A harness-mode chat does research and tool work, and may wait minutes for an approval.
77
- turnTimeoutMs: integer(data.turnTimeoutMs, users.some(user => user.directMode === 'harness') ? 900_000 : 120_000, 1000, 1_800_000, 'turnTimeoutMs'),
76
+ // An agent's work can take an hour. Only an administrator's explicit cap bounds restricted turns.
77
+ ...(data.turnTimeoutMs === undefined ? {} : { turnTimeoutMs: integer(data.turnTimeoutMs, 0, 1000, 1_800_000, 'turnTimeoutMs') }),
78
78
  idleTimeoutMs: integer(data.idleTimeoutMs, 1_800_000, 1000, 86_400_000, 'idleTimeoutMs'),
79
79
  maxSessions: integer(data.maxSessions, 100, 1, 1000, 'maxSessions'),
80
80
  maxSessionsPerUser: integer(data.maxSessionsPerUser, 10, 1, 100, 'maxSessionsPerUser'),
@@ -14,6 +14,9 @@ const TRUNCATED = '回复超过了飞书卡片的长度上限,后面的内容
14
14
  const grey = (markdown) => `<font color='grey'>${markdown}</font>`;
15
15
  const bytes = (value) => Buffer.byteLength(typeof value === 'string' ? value : JSON.stringify(value), 'utf8');
16
16
  const code = (value) => '`' + value.replace(/`/g, 'ˋ') + '`';
17
+ // A harness turn may run for an hour; past a minute, seconds stop being readable.
18
+ const elapsed = (seconds) => seconds < 60 ? `${seconds} 秒`
19
+ : seconds < 3600 ? `${Math.floor(seconds / 60)} 分 ${seconds % 60} 秒` : `${Math.floor(seconds / 3600)} 小时 ${Math.floor(seconds % 3600 / 60)} 分`;
17
20
  /** The App's step labels and a matching Feishu icon, by tool name. */
18
21
  function activity(name) {
19
22
  const is = (...names) => names.includes(name);
@@ -404,7 +407,7 @@ export class LiveReply {
404
407
  }
405
408
  panelTitle() {
406
409
  const tools = this.tools ? ` · ${this.tools} 次工具调用` : '';
407
- const seconds = (end) => Math.max(1, Math.round(((end ?? this.now()) - this.startedAt) / 1000));
410
+ const seconds = (end) => elapsed(Math.max(1, Math.round(((end ?? this.now()) - this.startedAt) / 1000)));
408
411
  switch (this.phase) {
409
412
  case 'thinking': {
410
413
  const running = [...this.steps].reverse().find(step => step.kind === 'tool' && step.state === 'running');
@@ -413,10 +416,10 @@ export class LiveReply {
413
416
  const detail = running.detail ? plainLabel(running.detail, 40) : '';
414
417
  return `${activity(running.name).doing}${detail ? ` · ${detail}` : '…'}`;
415
418
  }
416
- case 'answering': return `已思考 ${seconds(this.answeredAt)} 秒${tools}`;
417
- case 'done': return `已完成,用时 ${seconds(this.finishedAt)} 秒${tools}`;
418
- case 'stopped': return `已停止,用时 ${seconds(this.finishedAt)} 秒${tools}`;
419
- case 'failed': return `处理失败,用时 ${seconds(this.finishedAt)} 秒${tools}`;
419
+ case 'answering': return `已思考 ${seconds(this.answeredAt)}${tools}`;
420
+ case 'done': return `已完成,用时 ${seconds(this.finishedAt)}${tools}`;
421
+ case 'stopped': return `已停止,用时 ${seconds(this.finishedAt)}${tools}`;
422
+ case 'failed': return `处理失败,用时 ${seconds(this.finishedAt)}${tools}`;
420
423
  }
421
424
  }
422
425
  /** The folded process panel. Readers open it when they want the thinking and tool steps. */
@@ -39,6 +39,8 @@ export declare class ConnectionController {
39
39
  private unsafeStop;
40
40
  private menuReady;
41
41
  private documentsReady;
42
+ /** Menu entries the published app lacks; the owner adds them in the developer console. */
43
+ private menuMissing;
42
44
  private documentSetup?;
43
45
  private documentJob?;
44
46
  private documentAborter?;
@@ -9,6 +9,7 @@ import { LockHeldError } from '../lock.js';
9
9
  import { BOT_SCOPES, CARD_SCOPE, LarkPlatformError } from './lark-platform.js';
10
10
  import { createChallenge, PairingError, waitForPairing } from './pairing.js';
11
11
  import { defaultScopesReady } from '../rich-scopes.js';
12
+ import { missingBotMenuEntries } from '../bot-menu.js';
12
13
  const restoreDelay = (attempt) => Math.min(10 * 60_000, 5_000 * 2 ** Math.min(attempt, 10));
13
14
  const messages = {
14
15
  idle: '把助手带到飞书,先用飞书确认创建。',
@@ -75,6 +76,8 @@ export class ConnectionController {
75
76
  unsafeStop = false;
76
77
  menuReady = false;
77
78
  documentsReady = false;
79
+ /** Menu entries the published app lacks; the owner adds them in the developer console. */
80
+ menuMissing = [];
78
81
  documentSetup;
79
82
  documentJob;
80
83
  documentAborter;
@@ -87,7 +90,8 @@ export class ConnectionController {
87
90
  }
88
91
  snapshot() {
89
92
  return structuredClone({ ...this.state,
90
- capabilities: { ...this.state.capabilities, documents: this.documentsReady }, ...(this.documentSetup ? { documentSetup: this.documentSetup } : {}) });
93
+ capabilities: { ...this.state.capabilities, documents: this.documentsReady }, ...(this.documentSetup ? { documentSetup: this.documentSetup } : {}),
94
+ ...(this.record && this.menuMissing.length ? { menu: { missing: this.menuMissing, url: this.consoleUrl() } } : {}) });
91
95
  }
92
96
  phase(phase, message = messages[phase]) {
93
97
  this.state = { phase, message, busy: ['creating', 'configuring', 'starting'].includes(phase),
@@ -168,6 +172,7 @@ export class ConnectionController {
168
172
  if (!aborter.signal.aborted && !this.closed && this.record?.appId === record.appId) {
169
173
  if (inspection.appId === record.appId && (!record.botOpenId || inspection.botOpenId === record.botOpenId)) {
170
174
  this.documentsReady = inspection.scopeStatus === 'verified' && defaultScopesReady(inspection.grantedScopes);
175
+ this.menuMissing = missingBotMenuEntries(inspection.menuEventKeys ?? []);
171
176
  }
172
177
  }
173
178
  }).catch(() => { })
@@ -353,6 +358,7 @@ export class ConnectionController {
353
358
  let record = this.record;
354
359
  let inspection = await this.deps.platform.inspect(record.profile, signal);
355
360
  this.documentsReady = inspection.scopeStatus === 'verified' && defaultScopesReady(inspection.grantedScopes);
361
+ this.menuMissing = missingBotMenuEntries(inspection.menuEventKeys ?? []);
356
362
  this.assertCurrent(signal);
357
363
  if (inspection.appId !== record.appId || (record.botOpenId && inspection.botOpenId && record.botOpenId !== inspection.botOpenId)) {
358
364
  throw new SetupError('机器人身份发生变化,请断开后重新绑定。', false);
@@ -1,6 +1,7 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import { ConnectionController } from './controller.js';
3
3
  import type { ConnectionResult } from './protocol.js';
4
+ import { type Release } from './updates.js';
4
5
  interface Peer {
5
6
  principal?: {
6
7
  kind?: string;
@@ -29,11 +30,11 @@ export declare function registerConnectionRpc(rpc: RpcHost, controller: Connecti
29
30
  export declare function findLarkExecutable(explicit?: string): Promise<string>;
30
31
  /**
31
32
  * Attach the control plane without requiring any user-written team configuration.
32
- * `latestVersion` replaces the npm lookup behind the upgrade notice (tests use it to stay offline).
33
+ * `latestRelease` replaces the npm lookup behind the upgrade notice (tests use it to stay offline).
33
34
  */
34
35
  export declare function applyOnboarding(ctx: Context, options?: {
35
36
  dataDir?: string;
36
37
  executable?: string;
37
- latestVersion?: (signal: AbortSignal) => Promise<string | undefined>;
38
+ latestRelease?: (signal: AbortSignal) => Promise<Release | undefined>;
38
39
  }): Promise<void>;
39
40
  export {};
@@ -203,7 +203,7 @@ async function waitReady(service, signal) {
203
203
  }
204
204
  /**
205
205
  * Attach the control plane without requiring any user-written team configuration.
206
- * `latestVersion` replaces the npm lookup behind the upgrade notice (tests use it to stay offline).
206
+ * `latestRelease` replaces the npm lookup behind the upgrade notice (tests use it to stay offline).
207
207
  */
208
208
  export async function applyOnboarding(ctx, options = {}) {
209
209
  const connection = ctx.get('connection');
@@ -260,7 +260,7 @@ export async function applyOnboarding(ctx, options = {}) {
260
260
  }
261
261
  }, });
262
262
  const installed = installedPackage();
263
- const updates = new UpdateCheck(installed?.version, options.latestVersion
263
+ const updates = new UpdateCheck(installed?.version, options.latestRelease
264
264
  ?? (signal => installed ? npmLatest(installed.name, signal) : Promise.resolve(undefined)));
265
265
  const control = {
266
266
  snapshot: () => withUpdate(controller.snapshot(), updates),
@@ -37,6 +37,8 @@ export interface Inspection {
37
37
  pendingVersionId?: string;
38
38
  publicationStatus: 'approved' | 'pending' | 'rejected' | 'draft' | 'unknown';
39
39
  subscribedEvents?: string[];
40
+ /** Event keys of the shown bot menu in that version; empty when its menu is off. */
41
+ menuEventKeys?: string[];
40
42
  /** Read-only CLI preflight evidence. Actual connection readiness is checked by the service. */
41
43
  cardReady?: boolean;
42
44
  menuReady?: boolean;
@@ -429,6 +429,11 @@ export class LarkPlatform {
429
429
  if (Array.isArray(detail.events) && detail.events.every(v => typeof v === 'string' && /^[a-zA-Z0-9_.:-]{1,200}$/.test(v))) {
430
430
  inspection.subscribedEvents = detail.events;
431
431
  }
432
+ const bot = object(object(detail.ability)?.bot);
433
+ if (bot && Array.isArray(bot.bot_menus) && bot.bot_menus.length <= 64) {
434
+ inspection.menuEventKeys = bot.bot_menu_enable !== true ? [] : bot.bot_menus.map(node => object(node)?.event_key)
435
+ .filter((key) => typeof key === 'string' && /^[a-zA-Z0-9_.:-]{1,64}$/.test(key));
436
+ }
432
437
  });
433
438
  checkSignal(signal);
434
439
  return inspection;
@@ -40,6 +40,15 @@ export interface ConnectionSnapshot {
40
40
  expiresAt: number;
41
41
  };
42
42
  };
43
+ /** Bottom-menu entries the published app lacks, and where its owner adds them. */
44
+ menu?: {
45
+ missing: Array<{
46
+ label: string;
47
+ eventKey: string;
48
+ parent?: string;
49
+ }>;
50
+ url: string;
51
+ };
43
52
  /** A newer published release of this plugin, with the installed version. */
44
53
  update?: {
45
54
  current: string;
@@ -1,11 +1,21 @@
1
+ /** A published release and when npm says it was published (epoch milliseconds). */
2
+ export interface Release {
3
+ version: string;
4
+ publishedAt: number;
5
+ }
1
6
  /** True when `next` is a later release than `current`; a prerelease comes before its release. */
2
7
  export declare function newerVersion(next: string, current: string): boolean;
3
- /** npm's `latest` tag for a package, when it names a stable release. Sends only the package name. */
4
- export declare function npmLatest(name: string, signal: AbortSignal): Promise<string | undefined>;
8
+ /**
9
+ * npm's `latest` release of a package and its publish time, when it names a stable release.
10
+ * Only the full package document carries publish times. Sends only the package name.
11
+ */
12
+ export declare function npmLatest(name: string, signal: AbortSignal): Promise<Release | undefined>;
5
13
  /**
6
14
  * Whether a newer release of this plugin is available, for the settings page.
7
15
  * A status read never waits for the registry: it returns what is known and
8
16
  * starts a background check when one is due. Failures just show no notice.
17
+ * A release younger than a day is not offered yet, since installing it would
18
+ * still pick an older one; it shows up once it ages, without another check.
9
19
  */
10
20
  export declare class UpdateCheck {
11
21
  private readonly current;
@@ -14,7 +24,7 @@ export declare class UpdateCheck {
14
24
  private latest?;
15
25
  private nextCheck;
16
26
  private running;
17
- constructor(current: string | undefined, lookup: (signal: AbortSignal) => Promise<string | undefined>, now?: () => number);
27
+ constructor(current: string | undefined, lookup: (signal: AbortSignal) => Promise<Release | undefined>, now?: () => number);
18
28
  status(): {
19
29
  current: string;
20
30
  latest: string;
@@ -2,6 +2,9 @@
2
2
  const CHECK_EVERY_MS = 6 * 60 * 60_000;
3
3
  // A failed check (offline, blocked registry) waits this long before the next try.
4
4
  const RETRY_AFTER_MS = 30 * 60_000;
5
+ // pnpm 11, which the App and `dsh plugin` install with, skips releases younger than a day
6
+ // (`minimumReleaseAge`) and installs an older one instead, so a release is offered only after that.
7
+ const INSTALLABLE_AFTER_MS = 24 * 60 * 60_000;
5
8
  const VERSION = /^(\d{1,9})\.(\d{1,9})\.(\d{1,9})(?:-([0-9A-Za-z.-]{1,40}))?$/;
6
9
  /** True when `next` is a later release than `current`; a prerelease comes before its release. */
7
10
  export function newerVersion(next, current) {
@@ -15,19 +18,29 @@ export function newerVersion(next, current) {
15
18
  }
16
19
  return !a[4] && Boolean(b[4]);
17
20
  }
18
- /** npm's `latest` tag for a package, when it names a stable release. Sends only the package name. */
21
+ /**
22
+ * npm's `latest` release of a package and its publish time, when it names a stable release.
23
+ * Only the full package document carries publish times. Sends only the package name.
24
+ */
19
25
  export async function npmLatest(name, signal) {
20
- const response = await fetch(`https://registry.npmjs.org/-/package/${name.replace('/', '%2f')}/dist-tags`, { signal, headers: { accept: 'application/json' } });
26
+ const response = await fetch(`https://registry.npmjs.org/${name.replace('/', '%2f')}`, { signal, headers: { accept: 'application/json' } });
21
27
  if (!response.ok)
22
28
  return;
23
- const tags = await response.json();
24
- const latest = tags?.latest;
25
- return typeof latest === 'string' && VERSION.test(latest) && !latest.includes('-') ? latest : undefined;
29
+ const document = await response.json();
30
+ const latest = document?.['dist-tags']?.latest;
31
+ if (typeof latest !== 'string' || !VERSION.test(latest) || latest.includes('-'))
32
+ return;
33
+ const time = document?.time;
34
+ const stamp = time && typeof time === 'object' ? time[latest] : undefined;
35
+ const publishedAt = typeof stamp === 'string' ? Date.parse(stamp) : Number.NaN;
36
+ return Number.isFinite(publishedAt) ? { version: latest, publishedAt } : undefined;
26
37
  }
27
38
  /**
28
39
  * Whether a newer release of this plugin is available, for the settings page.
29
40
  * A status read never waits for the registry: it returns what is known and
30
41
  * starts a background check when one is due. Failures just show no notice.
42
+ * A release younger than a day is not offered yet, since installing it would
43
+ * still pick an older one; it shows up once it ages, without another check.
31
44
  */
32
45
  export class UpdateCheck {
33
46
  current;
@@ -46,15 +59,18 @@ export class UpdateCheck {
46
59
  return;
47
60
  if (!this.running && this.now() >= this.nextCheck)
48
61
  void this.refresh();
49
- return this.latest && newerVersion(this.latest, this.current) ? { current: this.current, latest: this.latest } : undefined;
62
+ const latest = this.latest;
63
+ if (!latest || this.now() - latest.publishedAt < INSTALLABLE_AFTER_MS)
64
+ return;
65
+ return newerVersion(latest.version, this.current) ? { current: this.current, latest: latest.version } : undefined;
50
66
  }
51
67
  async refresh() {
52
68
  this.running = true;
53
69
  this.nextCheck = this.now() + RETRY_AFTER_MS;
54
70
  try {
55
71
  const latest = await this.lookup(AbortSignal.timeout(10_000));
56
- if (latest && VERSION.test(latest)) {
57
- this.latest = latest;
72
+ if (latest && VERSION.test(latest.version) && Number.isFinite(latest.publishedAt)) {
73
+ this.latest = { version: latest.version, publishedAt: latest.publishedAt };
58
74
  this.nextCheck = this.now() + CHECK_EVERY_MS;
59
75
  }
60
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-lark-agent",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Reach your DeepSeek Harness assistant from Feishu / Lark: a dsh plugin built on the official Lark CLI",
5
5
  "keywords": [
6
6
  "deepseek",