dsh-lark-agent 0.3.2 → 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:
@@ -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);
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-lark-agent",
3
- "version": "0.3.2",
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",