opencode-feishu-plugin 0.2.12 → 0.2.14

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.en.md CHANGED
@@ -22,6 +22,7 @@ Connect [OpenCode](https://opencode.ai) to Feishu/Lark: **one Feishu topic = one
22
22
  | 📊 **Real-time visible** | "Thinking" receipt → live tool-call cards → streaming text updates; footer shows the current model |
23
23
  | ⏹ **Controllable** | Every reply card has a "force stop" button; a watchdog auto-interrupts stuck sessions; native queue with `/steer` `/now` to cut in |
24
24
  | 📎 **Images / files** | Images and files sent in Feishu are downloaded and attached to the session, so vision/file-capable models can see and read them |
25
+ | 🧭 **One-sentence session** | Send a task in the main chat — the AI judges intent, finds the right working directory, and creates + starts the session in one tap |
25
26
  | 🖱 **Main-window menu** | Two fixed quick buttons above the chat input — "New session" and "Session list" (bot custom menu) — one tap to fire them |
26
27
  | 🚫 **No ports** | Full long-connection; no inbound port needed on the server |
27
28
 
@@ -127,6 +128,22 @@ After configuring the menu (setup step 6), two quick buttons stay pinned **above
127
128
  - After changing the menu, **re-publish the app version** (per the console: takes effect within ~5 minutes after publishing);
128
129
  - Message the bot at least once before using the menu (the plugin remembers the p2p chat from messages; any normal usage satisfies this).
129
130
 
131
+ ### One-sentence session creation (AI finds the directory)
132
+
133
+ When you send plain text in the main chat, the AI first judges the **intent** and picks the best-matching working directory from candidates (recent dirs + local session dirs), then replies with a proposal card:
134
+
135
+ ```
136
+ You: fix the zlib download bug
137
+ → 🆕 Suggested new session
138
+ Task: fix download bug | Dir: `/Users/code/zlib`
139
+ [✅ Create & send] [❌ Cancel]
140
+ ```
141
+
142
+ "Create & send" = create the session + auto-open a topic + send your original message in to start working (permission defaults to "Editable"; adjust later with `/perm`).
143
+
144
+ - Chat/unrecognized/dir-not-in-candidates → falls back to the console hint card, **never creates a wrong session**; the model's path must match a candidate (anti-hallucination);
145
+ - Proposal cards are valid for 1 hour; set `quickNew: false` to go back to the console-only mode.
146
+
130
147
  ### Inside a topic (work)
131
148
 
132
149
  A topic = a session; sending plain text is giving the AI a command.
@@ -174,6 +191,7 @@ When the agent asks via `question` or other form tools, the form becomes a Feish
174
191
  | `logFile` | string \| boolean | — | `true` = write `<configDir>/plugins/feishu.log`; recommended in server mode |
175
192
  | `approvalTtlMs` | number | `600000` | Approval token / card validity |
176
193
  | `staleExecutionMs` | number | `300000` | Watchdog threshold (0–60 min; **0 = disabled**; sessions waiting on a form / pending approval are never killed) |
194
+ | `quickNew` | boolean | `true` | Main-chat "one-sentence session": AI judges intent + finds the directory, proposal card creates in one tap; `false` = console-only mode |
177
195
  | `gatewayLocation` | string | — | Only start the gateway at this location (or its subdirectories); empty = any location |
178
196
 
179
197
  Full config (including `cardMaxTables`, `topicStatus*`, `resumeSummary*`, `keepalive*`, `gatewayMatchGraceMs`) → [docs/advanced.en.md](./docs/advanced.en.md#full-configuration).
package/README.md CHANGED
@@ -22,6 +22,7 @@
22
22
  | 📊 **实时可见** | 「思考中」回执 → 工具调用实时上卡 → 文本流式更新,页脚显示当前模型 |
23
23
  | ⏹ **可控可停** | 每张回复卡带「强制停止」;看门狗自动中断卡死会话;忙时原生排队,`/steer` `/now` 可插队 |
24
24
  | 📎 **图片 / 文件** | 飞书里的图片 / 文件自动下载并挂进会话,支持视觉 / 文件的模型直接看图、读文件 |
25
+ | 🧭 **一句话建会话** | 主聊天流发任务文本,AI 自动判断意图、找好工作目录,一键建会话并开始处理 |
25
26
  | 🖱 **主窗口菜单** | 机器人输入框上固定「新建会话」「会话列表」快捷按钮(机器人自定义菜单),一点即用 |
26
27
  | 🚫 **无端口** | 全程长连接,服务器无需开放任何入站端口 |
27
28
 
@@ -127,6 +128,22 @@ tail -f ~/.config/opencode/plugins/feishu.log # 应看到「飞书长连接已
127
128
  - 菜单配置修改后需**重新发布版本**(官方说明:发版成功后约 5 分钟内生效);
128
129
  - 首次使用菜单前,先给机器人发过至少一条消息(插件据消息记住单聊会话;正常使用必然满足)。
129
130
 
131
+ ### 一句话建会话(AI 找目录)
132
+
133
+ 主聊天流发普通文本时,AI 会先判断**意图**,并从候选目录(最近使用 + 本机会话目录)里挑出最匹配的工作目录,回一张建议卡:
134
+
135
+ ```
136
+ 你:帮我修一下 zlib 的下载 bug
137
+ → 🆕 建议新建会话
138
+ 任务:修下载 bug | 目录:`/Users/code/zlib`
139
+ [✅ 创建并发送] [❌ 取消]
140
+ ```
141
+
142
+ 点「创建并发送」= 建会话 + 自动开话题 + 把原消息发进去开始处理(权限默认「可编辑」,进话题后可用 `/perm` 调整)。
143
+
144
+ - 闲聊 / 无法识别 / 目录不在候选中 → 回退为管理台提示卡,**不会误建会话**;模型给出的路径必须命中候选清单(防幻觉);
145
+ - 建议卡有效期 1 小时;不想要此功能可设 `quickNew: false` 回到纯管理台模式。
146
+
130
147
  ### 话题内(干活)
131
148
 
132
149
  一个话题 = 一个会话,发普通文本就是给 AI 下指令。
@@ -174,6 +191,7 @@ agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发
174
191
  | `logFile` | string \| boolean | — | `true` = 写 `<configDir>/plugins/feishu.log`,服务模式建议开启 |
175
192
  | `approvalTtlMs` | number | `600000` | 审批 token / 卡片有效期 |
176
193
  | `staleExecutionMs` | number | `300000` | 看门狗阈值(0–60 分钟;**0 = 关闭看门狗**;待答表单 / 未决审批期间不判卡死) |
194
+ | `quickNew` | boolean | `true` | 主聊天流「一句话建会话」:AI 判断意图 + 找目录,建议卡一键创建;`false` = 回到纯管理台模式 |
177
195
  | `gatewayLocation` | string | — | 只在该 location(及其子目录)启动网关;留空 = 任意 location 生效 |
178
196
 
179
197
  完整配置(含 `cardMaxTables`、`topicStatus*`、`resumeSummary*`、`keepalive*`、`gatewayMatchGraceMs` 等进阶项)见 [docs/advanced.md](./docs/advanced.md#完整配置项)。
package/dist/index.js CHANGED
@@ -144037,6 +144037,7 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
144037
144037
  );
144038
144038
  const attachmentTimeoutMs = clamp4(asNumber(merged.attachmentTimeoutMs, 3e4), 5e3, 12e4);
144039
144039
  const attachmentsDirRaw = asString(merged.attachmentsDir).trim();
144040
+ const quickNew = asBoolean(merged.quickNew, true);
144040
144041
  const keepaliveIntervalMs = clamp4(
144041
144042
  asNumber(merged.keepaliveIntervalMs, 20 * 60 * 1e3),
144042
144043
  5 * 60 * 1e3,
@@ -144101,7 +144102,8 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
144101
144102
  acceptAttachments,
144102
144103
  attachmentMaxBytes,
144103
144104
  attachmentTimeoutMs,
144104
- ...attachmentsDirRaw ? { attachmentsDir: attachmentsDirRaw } : {}
144105
+ ...attachmentsDirRaw ? { attachmentsDir: attachmentsDirRaw } : {},
144106
+ quickNew
144105
144107
  };
144106
144108
  }
144107
144109
  function loadConfigFile(env, deps, warnings) {
@@ -146078,6 +146080,14 @@ function withAssistantID(state, assistantMessageID) {
146078
146080
  if (!assistantMessageID || assistantMessageID === state.assistantMessageID) return state;
146079
146081
  return { ...state, assistantMessageID };
146080
146082
  }
146083
+ function lastPrefixTextIndex(blocks, text) {
146084
+ for (let i = blocks.length - 1; i >= 0; i -= 1) {
146085
+ const block = blocks[i];
146086
+ if (block.kind !== "text") continue;
146087
+ if (block.content && text.startsWith(block.content)) return i;
146088
+ }
146089
+ return -1;
146090
+ }
146081
146091
  function patchTool(state, id2, update2) {
146082
146092
  let found = false;
146083
146093
  const blocks = state.blocks.map((b) => {
@@ -146118,21 +146128,62 @@ function reduce6(state, event) {
146118
146128
  if (!event.delta) return state;
146119
146129
  const base2 = stepTransition(state, event.assistantMessageID);
146120
146130
  const last = base2[base2.length - 1];
146121
- const blocks = last && last.kind === "text" && last.streaming ? [...base2.slice(0, -1), { ...last, content: last.content + event.delta }] : [...base2, { kind: "text", content: event.delta, streaming: true }];
146131
+ const blocks = last && last.kind === "text" && last.streaming ? [...base2.slice(0, -1), { ...last, content: last.content + event.delta, ...event.assistantMessageID ? { msg: event.assistantMessageID } : {} }] : [...base2, { kind: "text", content: event.delta, streaming: true, ...event.assistantMessageID ? { msg: event.assistantMessageID } : {} }];
146122
146132
  const next = withAssistantID(state, event.assistantMessageID);
146123
146133
  return { ...next, blocks, footer: "streaming", terminal: "running" };
146124
146134
  }
146125
146135
  case "text.ended": {
146126
146136
  const blocks = state.blocks;
146127
- const last = blocks[blocks.length - 1];
146128
- let nextBlocks;
146129
- if (last && last.kind === "text" && last.streaming) {
146130
- nextBlocks = [...blocks.slice(0, -1), { ...last, content: event.text ?? last.content, streaming: false }];
146131
- } else if (event.text) {
146132
- nextBlocks = [...closeStreamingText(blocks), { kind: "text", content: event.text, streaming: false }];
146133
- } else {
146134
- nextBlocks = closeStreamingText(blocks);
146137
+ const endedText = event.text;
146138
+ const endedMsg = event.assistantMessageID;
146139
+ if (!endedMsg) {
146140
+ const last = blocks[blocks.length - 1];
146141
+ let nextBlocks2;
146142
+ if (last && last.kind === "text" && last.streaming) {
146143
+ nextBlocks2 = [...blocks.slice(0, -1), { ...last, content: endedText ?? last.content, streaming: false }];
146144
+ } else if (endedText) {
146145
+ const idx = lastPrefixTextIndex(blocks, endedText);
146146
+ nextBlocks2 = idx >= 0 ? blocks.map((b, i) => i === idx && b.kind === "text" ? { ...b, content: endedText, streaming: false } : b) : [...closeStreamingText(blocks), { kind: "text", content: endedText, streaming: false }];
146147
+ } else {
146148
+ nextBlocks2 = closeStreamingText(blocks);
146149
+ }
146150
+ return { ...state, blocks: nextBlocks2, footer: "streaming" };
146135
146151
  }
146152
+ let targetIdx = -1;
146153
+ for (let i = blocks.length - 1; i >= 0; i -= 1) {
146154
+ const b = blocks[i];
146155
+ if (b.kind === "text" && b.msg === endedMsg) {
146156
+ targetIdx = i;
146157
+ break;
146158
+ }
146159
+ }
146160
+ const tail = blocks[blocks.length - 1];
146161
+ if (targetIdx < 0 && tail && tail.kind === "text" && tail.streaming) targetIdx = blocks.length - 1;
146162
+ if (targetIdx < 0) {
146163
+ if (!endedText) return { ...state, blocks: closeStreamingText(blocks), footer: "streaming" };
146164
+ return {
146165
+ ...state,
146166
+ blocks: [
146167
+ ...closeStreamingText(blocks),
146168
+ { kind: "text", content: endedText, streaming: false, msg: endedMsg }
146169
+ ],
146170
+ footer: "streaming"
146171
+ };
146172
+ }
146173
+ const target = blocks[targetIdx];
146174
+ let earlierLen = 0;
146175
+ for (let i = 0; i < targetIdx; i += 1) {
146176
+ const b = blocks[i];
146177
+ if (b.kind === "text" && b.msg === endedMsg) earlierLen += b.content.length;
146178
+ }
146179
+ let content = target.kind === "text" ? target.content : "";
146180
+ if (endedText !== void 0 && endedText.length >= earlierLen) {
146181
+ const suffix = endedText.slice(earlierLen);
146182
+ if (suffix.length >= content.length) content = suffix;
146183
+ }
146184
+ const nextBlocks = blocks.map(
146185
+ (b, i) => i === targetIdx && b.kind === "text" ? { ...b, content, streaming: false } : b
146186
+ );
146136
146187
  return { ...state, blocks: nextBlocks, footer: "streaming" };
146137
146188
  }
146138
146189
  case "tool.input.started": {
@@ -146625,13 +146676,49 @@ function trailingText(blocks) {
146625
146676
  }
146626
146677
  return parts.join("\n\n").trim();
146627
146678
  }
146679
+ function isActivityEvent(type) {
146680
+ return type === "text.started" || type === "text.delta" || type === "text.ended" || type === "tool.input.started" || type === "tool.input.ended" || type === "tool.success" || type === "tool.error";
146681
+ }
146682
+ var QUEUED_DRAIN_GRACE_MS = 3e3;
146628
146683
  function createRunController(deps) {
146629
146684
  const sessions = /* @__PURE__ */ new Map();
146630
146685
  const throttlers = /* @__PURE__ */ new Map();
146631
146686
  const sessionModels = /* @__PURE__ */ new Map();
146632
146687
  const notifiedQueued = /* @__PURE__ */ new Set();
146688
+ const drainTimers = /* @__PURE__ */ new Map();
146633
146689
  const throttleMs = Math.max(400, deps.throttleMs);
146634
146690
  let disposed = false;
146691
+ const cancelDrain = (sessionID) => {
146692
+ const timer = drainTimers.get(sessionID);
146693
+ if (timer) {
146694
+ clearTimeout(timer);
146695
+ drainTimers.delete(sessionID);
146696
+ }
146697
+ };
146698
+ const scheduleDrain = (sessionID, runs) => {
146699
+ if (runs.queued.length === 0) {
146700
+ cancelDrain(sessionID);
146701
+ return;
146702
+ }
146703
+ cancelDrain(sessionID);
146704
+ const timer = setTimeout(() => {
146705
+ drainTimers.delete(sessionID);
146706
+ if (disposed) return;
146707
+ const live = sessions.get(sessionID);
146708
+ if (!live || live.queued.length === 0) return;
146709
+ deps.log.info("\u6392\u961F\u5361\u6309\u300C\u5DF2\u968F\u672C\u8F6E\u5904\u7406\u300D\u6536\u5C3E\uFF08\u961F\u5217\u6D88\u606F\u5728\u5F53\u524D\u6267\u884C\u5185\u88AB\u6D88\u8D39\uFF09", {
146710
+ sessionID,
146711
+ count: live.queued.length
146712
+ });
146713
+ for (const card of live.queued) {
146714
+ card.state = reduce6(card.state, { type: "execution.succeeded" });
146715
+ finalize2(card);
146716
+ }
146717
+ live.queued.length = 0;
146718
+ }, QUEUED_DRAIN_GRACE_MS);
146719
+ timer.unref?.();
146720
+ drainTimers.set(sessionID, timer);
146721
+ };
146635
146722
  const sessionRuns = (sessionID) => {
146636
146723
  let runs = sessions.get(sessionID);
146637
146724
  if (!runs) {
@@ -146739,6 +146826,7 @@ function createRunController(deps) {
146739
146826
  if (!runs) return;
146740
146827
  if (event.type === "execution.started") {
146741
146828
  notifiedQueued.delete(sessionID);
146829
+ cancelDrain(sessionID);
146742
146830
  if (runs.active) {
146743
146831
  update2(runs.active, event, true);
146744
146832
  return;
@@ -146746,41 +146834,62 @@ function createRunController(deps) {
146746
146834
  const next = runs.queued.shift();
146747
146835
  if (next) {
146748
146836
  runs.active = next;
146837
+ deps.log.debug("\u6392\u961F\u5361\u664B\u5347\u4E3A\u8FD0\u884C\u5361", { sessionID, runID: next.runID });
146749
146838
  update2(next, event, true);
146750
146839
  }
146840
+ if (runs.queued.length > 0) scheduleDrain(sessionID, runs);
146751
146841
  return;
146752
146842
  }
146843
+ if (!runs.active && runs.queued.length > 0 && isActivityEvent(event.type)) {
146844
+ const next = runs.queued.shift();
146845
+ runs.active = next;
146846
+ deps.log.info("\u6536\u5230\u6D3B\u52A8\u4E8B\u4EF6\u4F46\u65E0\u8FD0\u884C\u5361\uFF1A\u664B\u5347\u961F\u9996\u6392\u961F\u5361", {
146847
+ sessionID,
146848
+ runID: next.runID,
146849
+ event: event.type
146850
+ });
146851
+ next.state = reduce6(next.state, { type: "execution.started" });
146852
+ }
146753
146853
  const card = runs.active;
146754
- if (!card) return;
146755
- if (event.type === "execution.succeeded") {
146756
- const text = trailingText(card.state.blocks);
146757
- const long = deps.sendFinalAnswer !== void 0 && deps.finalAnswer !== void 0 && text.length >= deps.finalAnswer.minChars;
146758
- if (long) {
146759
- card.state = reduce6(reduce6(card.state, { type: "final.separated" }), event);
146854
+ if (event.type === "execution.succeeded" || event.type === "execution.failed") {
146855
+ if (card) {
146856
+ if (event.type === "execution.succeeded") {
146857
+ const text = trailingText(card.state.blocks);
146858
+ const long = deps.sendFinalAnswer !== void 0 && deps.finalAnswer !== void 0 && text.length >= deps.finalAnswer.minChars;
146859
+ if (long) {
146860
+ card.state = reduce6(reduce6(card.state, { type: "final.separated" }), event);
146861
+ finalize2(card);
146862
+ runs.active = void 0;
146863
+ void deps.sendFinalAnswer({
146864
+ sessionID,
146865
+ chatId: card.chatId,
146866
+ ...card.replyToMessageId ? { replyToMessageId: card.replyToMessageId } : {},
146867
+ text
146868
+ }).catch((err2) => {
146869
+ deps.log.warn("\u6700\u7EC8\u7B54\u6848\u53D1\u9001\u5931\u8D25", { sessionID, error: errorMessage(err2) });
146870
+ });
146871
+ scheduleDrain(sessionID, runs);
146872
+ return;
146873
+ }
146874
+ }
146875
+ update2(card, event, true);
146760
146876
  finalize2(card);
146761
146877
  runs.active = void 0;
146762
- void deps.sendFinalAnswer({
146763
- sessionID,
146764
- chatId: card.chatId,
146765
- ...card.replyToMessageId ? { replyToMessageId: card.replyToMessageId } : {},
146766
- text
146767
- }).catch((err2) => {
146768
- deps.log.warn("\u6700\u7EC8\u7B54\u6848\u53D1\u9001\u5931\u8D25", { sessionID, error: errorMessage(err2) });
146769
- });
146770
- return;
146771
146878
  }
146772
- update2(card, event, true);
146773
- finalize2(card);
146774
- runs.active = void 0;
146775
- return;
146776
- }
146777
- if (event.type === "execution.failed") {
146778
- update2(card, event, true);
146779
- finalize2(card);
146780
- runs.active = void 0;
146879
+ scheduleDrain(sessionID, runs);
146781
146880
  return;
146782
146881
  }
146882
+ if (!card) return;
146783
146883
  update2(card, event, false);
146884
+ if (isActivityEvent(event.type)) {
146885
+ deps.log.debug("\u8FD0\u884C\u5361\u5757\u7ED3\u6784", {
146886
+ sessionID,
146887
+ event: event.type,
146888
+ blocks: card.state.blocks.map(
146889
+ (b) => b.kind === "text" ? `T${b.streaming ? "*" : ""}${b.content.length}` : `tool:${b.tool.name}`
146890
+ )
146891
+ });
146892
+ }
146784
146893
  },
146785
146894
  hasActive(sessionID) {
146786
146895
  return sessions.get(sessionID)?.active !== void 0;
@@ -146818,6 +146927,8 @@ function createRunController(deps) {
146818
146927
  disposed = true;
146819
146928
  sessionModels.clear();
146820
146929
  notifiedQueued.clear();
146930
+ for (const timer of drainTimers.values()) clearTimeout(timer);
146931
+ drainTimers.clear();
146821
146932
  for (const throttler of throttlers.values()) throttler.cancel();
146822
146933
  throttlers.clear();
146823
146934
  for (const runs of sessions.values()) {
@@ -148451,6 +148562,128 @@ function parsePage(value3) {
148451
148562
  const parsed = typeof value3 === "number" ? value3 : typeof value3 === "string" ? Number.parseInt(value3, 10) : Number.NaN;
148452
148563
  return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
148453
148564
  }
148565
+
148566
+ // src/feishu/quick-new-cards.ts
148567
+ function button3(text, type, value3) {
148568
+ return {
148569
+ tag: "button",
148570
+ text: { tag: "plain_text", content: text },
148571
+ type,
148572
+ behaviors: [{ type: "callback", value: value3 }]
148573
+ };
148574
+ }
148575
+ function buildQuickNewThinkingCard() {
148576
+ return {
148577
+ schema: "2.0",
148578
+ config: { update_multi: true },
148579
+ header: { title: { tag: "plain_text", content: "\u{1F914} \u6B63\u5728\u8BC6\u522B\u610F\u56FE\u4E0E\u76EE\u5F55\u2026" }, template: "blue" },
148580
+ body: {
148581
+ elements: [
148582
+ { tag: "markdown", content: "\u6B63\u5728\u5224\u65AD\u8FD9\u6761\u6D88\u606F\u662F\u5426\u9700\u8981\u65B0\u5EFA\u4F1A\u8BDD\uFF0C\u4EE5\u53CA\u6700\u5408\u9002\u7684\u5DE5\u4F5C\u76EE\u5F55\u2026" }
148583
+ ]
148584
+ }
148585
+ };
148586
+ }
148587
+ function buildQuickNewProposalCard(input) {
148588
+ const preview = input.textPreview.trim().replace(/\n+/g, " ").slice(0, 120);
148589
+ const lines = [
148590
+ `**\u4EFB\u52A1**\uFF1A${input.title}`,
148591
+ `**\u76EE\u5F55**\uFF1A\`${input.directory}\``,
148592
+ ...input.reason ? [`**\u4F9D\u636E**\uFF1A${input.reason}`] : [],
148593
+ "",
148594
+ `\u539F\u6D88\u606F\uFF1A${preview}`
148595
+ ];
148596
+ const elements = [
148597
+ { tag: "markdown", content: truncateCardContent(lines.join("\n")) },
148598
+ {
148599
+ tag: "markdown",
148600
+ content: "\u70B9\u300C\u521B\u5EFA\u5E76\u53D1\u9001\u300D= \u5EFA\u4F1A\u8BDD + \u81EA\u52A8\u5F00\u8BDD\u9898 + \u628A\u539F\u6D88\u606F\u53D1\u8FDB\u53BB\u5F00\u59CB\u5904\u7406\u3002",
148601
+ text_size: "notation"
148602
+ },
148603
+ button3("\u2705 \u521B\u5EFA\u5E76\u53D1\u9001", "primary", { cmd: "quicknew", op: "create", id: input.id }),
148604
+ button3("\u274C \u53D6\u6D88", "default", { cmd: "quicknew", op: "cancel", id: input.id })
148605
+ ];
148606
+ return {
148607
+ schema: "2.0",
148608
+ config: { update_multi: true },
148609
+ header: { title: { tag: "plain_text", content: "\u{1F195} \u5EFA\u8BAE\u65B0\u5EFA\u4F1A\u8BDD" }, template: "turquoise" },
148610
+ body: { elements }
148611
+ };
148612
+ }
148613
+ function buildQuickNewResolvedCard(title, note2, template2 = "grey") {
148614
+ return {
148615
+ schema: "2.0",
148616
+ config: { update_multi: true },
148617
+ header: { title: { tag: "plain_text", content: title }, template: template2 },
148618
+ body: { elements: [{ tag: "markdown", content: truncateCardContent(note2) }] }
148619
+ };
148620
+ }
148621
+ function parseQuickNewActionValue(raw) {
148622
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
148623
+ const value3 = raw;
148624
+ if (value3.cmd !== "quicknew") return void 0;
148625
+ const op = value3.op === "create" || value3.op === "cancel" ? value3.op : void 0;
148626
+ const id2 = typeof value3.id === "string" ? value3.id.trim() : "";
148627
+ if (!op || !id2) return void 0;
148628
+ return { op, id: id2 };
148629
+ }
148630
+
148631
+ // src/session/quick-new.ts
148632
+ var QUICK_NEW_INSTRUCTION = [
148633
+ "\u4F60\u662F\u98DE\u4E66 AI \u52A9\u624B\u300C\u7BA1\u7406\u53F0\u300D\u7684\u610F\u56FE\u8BC6\u522B\u5668\u3002\u7528\u6237\u5728\u7BA1\u7406\u53F0\uFF08\u8FD8\u6CA1\u6709\u4F1A\u8BDD\uFF09\u53D1\u6765\u4E00\u6761\u6D88\u606F\u3002",
148634
+ "\u8BF7\u5224\u65AD\u5B83\u662F\u4E0D\u662F\u4E00\u4E2A\u9700\u8981\u65B0\u5EFA\u4F1A\u8BDD\u53BB\u6267\u884C\u7684\u5F00\u53D1/\u64CD\u4F5C\u4EFB\u52A1\uFF0C\u5E76\u4ECE\u5019\u9009\u76EE\u5F55\u4E2D\u9009\u51FA\u6700\u5408\u9002\u7684\u5DE5\u4F5C\u76EE\u5F55\u3002",
148635
+ "\u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\uFF1A",
148636
+ '{"intent":"task|chat","dir":"<\u7EDD\u5BF9\u8DEF\u5F84\u6216\u7A7A\u5B57\u7B26\u4E32>","title":"<\u4E0D\u8D85\u8FC720\u5B57\u7684\u4F1A\u8BDD\u6807\u9898>","reason":"<\u4E00\u53E5\u8BDD\u7406\u7531>"}',
148637
+ "\u89C4\u5219\uFF1A",
148638
+ "- \u95F2\u804A\u3001\u95EE\u5019\u3001\u8BE2\u95EE\u7528\u6CD5 \u2192 intent=chat\uFF0Cdir \u7559\u7A7A\u3002",
148639
+ "- \u660E\u786E\u7684\u5F00\u53D1/\u64CD\u4F5C\u4EFB\u52A1\uFF08\u6539 bug\u3001\u5199\u4EE3\u7801\u3001\u6574\u7406\u6587\u4EF6\u3001\u8DD1\u811A\u672C\u7B49\uFF09\u2192 intent=task\u3002",
148640
+ "- dir \u53EA\u80FD\u4ECE\u5019\u9009\u76EE\u5F55\u4E2D\u9009\u62E9\uFF08\u8BED\u4E49\u6700\u5339\u914D\u7684\u90A3\u4E2A\uFF09\uFF1B\u627E\u4E0D\u5230\u5408\u9002\u7684\u5C31\u7559\u7A7A\u5B57\u7B26\u4E32\u3002"
148641
+ ].join("\n");
148642
+ function buildQuickNewPrompt(text, candidates) {
148643
+ const list = candidates.slice(0, 40).map((c) => `- ${c.path}${c.label ? `\uFF08${c.label}\uFF09` : ""}`).join("\n");
148644
+ return [
148645
+ QUICK_NEW_INSTRUCTION,
148646
+ "",
148647
+ "\u5019\u9009\u76EE\u5F55\uFF1A",
148648
+ list || "\uFF08\u65E0\uFF09",
148649
+ "",
148650
+ "\u7528\u6237\u6D88\u606F\uFF1A",
148651
+ text.slice(0, 2e3)
148652
+ ].join("\n");
148653
+ }
148654
+ function parseQuickNewDecision(raw) {
148655
+ if (!raw) return void 0;
148656
+ const text = raw.trim();
148657
+ const start2 = text.indexOf("{");
148658
+ const end = text.lastIndexOf("}");
148659
+ if (start2 < 0 || end <= start2) return void 0;
148660
+ let parsed;
148661
+ try {
148662
+ parsed = JSON.parse(text.slice(start2, end + 1));
148663
+ } catch {
148664
+ return void 0;
148665
+ }
148666
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
148667
+ const obj = parsed;
148668
+ const intent = obj.intent === "task" ? "task" : obj.intent === "chat" ? "chat" : void 0;
148669
+ if (!intent) return void 0;
148670
+ const dir = typeof obj.dir === "string" ? obj.dir.trim() : typeof obj.directory === "string" ? obj.directory.trim() : "";
148671
+ const title = typeof obj.title === "string" ? obj.title.trim().slice(0, 30) : "";
148672
+ const reason = typeof obj.reason === "string" ? obj.reason.trim().slice(0, 200) : "";
148673
+ return {
148674
+ intent,
148675
+ ...dir ? { directory: dir } : {},
148676
+ ...title ? { title } : {},
148677
+ ...reason ? { reason } : {}
148678
+ };
148679
+ }
148680
+ function matchCandidateDirectory(directory, candidates) {
148681
+ if (!directory) return void 0;
148682
+ const normalize2 = (p) => p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
148683
+ const target = normalize2(directory);
148684
+ const hit = candidates.find((c) => normalize2(c.path) === target);
148685
+ return hit?.path;
148686
+ }
148454
148687
  var SYSTEM_DIRS = [
148455
148688
  "/etc",
148456
148689
  "/usr",
@@ -149202,7 +149435,7 @@ var SETUP_FORM_FIELDS = {
149202
149435
  var SETUP_FORM_MAX_MODELS = 15;
149203
149436
  var SETUP_FORM_DEFAULT_PERM = "edit";
149204
149437
  var SETUP_FORM_DIR_CUSTOM = "__custom__";
149205
- function button3(text, type, value3) {
149438
+ function button4(text, type, value3) {
149206
149439
  return {
149207
149440
  tag: "button",
149208
149441
  text: { tag: "plain_text", content: text },
@@ -149245,28 +149478,28 @@ function buildModelCard(input) {
149245
149478
  if (input.page <= 0 && recent.length > 0) {
149246
149479
  for (const model of recent) {
149247
149480
  const isCurrent = input.current && refKey(input.current) === refKey(model);
149248
- elements.push(button3(`\u{1F9E0} ${shorten(modelLabel(model), 30)}`, isCurrent ? "primary" : "default", modelValue(model, input.sid)));
149481
+ elements.push(button4(`\u{1F9E0} ${shorten(modelLabel(model), 30)}`, isCurrent ? "primary" : "default", modelValue(model, input.sid)));
149249
149482
  }
149250
149483
  if (input.models.length > recent.length) {
149251
- elements.push(button3("\u66F4\u591A \u25B8", "default", moreValue(1, input.sid)));
149484
+ elements.push(button4("\u66F4\u591A \u25B8", "default", moreValue(1, input.sid)));
149252
149485
  }
149253
149486
  } else {
149254
149487
  const pageNum = input.page <= 0 ? 1 : input.page;
149255
149488
  const slice = input.models.slice((pageNum - 1) * pageSize, pageNum * pageSize);
149256
149489
  if (slice.length === 0 && recent.length > 0) {
149257
149490
  for (const model of recent) {
149258
- elements.push(button3(`\u{1F9E0} ${shorten(modelLabel(model), 30)}`, "default", modelValue(model, input.sid)));
149491
+ elements.push(button4(`\u{1F9E0} ${shorten(modelLabel(model), 30)}`, "default", modelValue(model, input.sid)));
149259
149492
  }
149260
149493
  } else {
149261
149494
  for (const model of slice) {
149262
149495
  const isCurrent = input.current && refKey(input.current) === refKey(model);
149263
149496
  elements.push(
149264
- button3(`\u{1F9E0} ${shorten(modelLabel(model), 30)}`, isCurrent ? "primary" : "default", modelValue(model, input.sid))
149497
+ button4(`\u{1F9E0} ${shorten(modelLabel(model), 30)}`, isCurrent ? "primary" : "default", modelValue(model, input.sid))
149265
149498
  );
149266
149499
  }
149267
- if (pageNum > 1) elements.push(button3("\u25C2 \u4E0A\u4E00\u9875", "default", moreValue(pageNum - 1, input.sid)));
149500
+ if (pageNum > 1) elements.push(button4("\u25C2 \u4E0A\u4E00\u9875", "default", moreValue(pageNum - 1, input.sid)));
149268
149501
  if (pageNum * pageSize < input.models.length) {
149269
- elements.push(button3("\u4E0B\u4E00\u9875 \u25B8", "default", moreValue(pageNum + 1, input.sid)));
149502
+ elements.push(button4("\u4E0B\u4E00\u9875 \u25B8", "default", moreValue(pageNum + 1, input.sid)));
149270
149503
  }
149271
149504
  }
149272
149505
  }
@@ -149280,7 +149513,7 @@ function buildPermCard(input) {
149280
149513
  }
149281
149514
  const elements = [{ tag: "markdown", content: truncateCardContent(lines.join("\n")) }];
149282
149515
  for (const info of PERMISSION_PRESETS) {
149283
- elements.push(button3(presetLabel(info.id), input.current === info.id ? "primary" : "default", permValue(info.id, input.sid)));
149516
+ elements.push(button4(presetLabel(info.id), input.current === info.id ? "primary" : "default", permValue(info.id, input.sid)));
149284
149517
  }
149285
149518
  return headerCard("\u{1F510} \u9009\u62E9\u6743\u9650\u6863\u4F4D", "orange", elements);
149286
149519
  }
@@ -149296,8 +149529,8 @@ function buildConfirmCard(input) {
149296
149529
  ];
149297
149530
  return headerCard("\u2705 \u786E\u8BA4\u521B\u5EFA\u4F1A\u8BDD", "green", [
149298
149531
  { tag: "markdown", content: truncateCardContent(lines.join("\n")) },
149299
- button3("\u2705 \u521B\u5EFA", "primary", { wizard: "confirm" }),
149300
- button3("\u2716\uFE0F \u53D6\u6D88", "default", { wizard: "cancel" })
149532
+ button4("\u2705 \u521B\u5EFA", "primary", { wizard: "confirm" }),
149533
+ button4("\u2716\uFE0F \u53D6\u6D88", "default", { wizard: "cancel" })
149301
149534
  ]);
149302
149535
  }
149303
149536
  function buildSetupDoneCard(title, lines, template2 = "grey") {
@@ -150857,6 +151090,7 @@ async function routeEvent(event, deps) {
150857
151090
  break;
150858
151091
  case "session.text.started": {
150859
151092
  const data = event.data;
151093
+ deps.log.debug("text.started", { sessionID: data.sessionID, msg: data.assistantMessageID });
150860
151094
  deps.applyRun(data.sessionID, {
150861
151095
  type: "text.started",
150862
151096
  ...data.assistantMessageID ? { assistantMessageID: data.assistantMessageID } : {}
@@ -150865,6 +151099,11 @@ async function routeEvent(event, deps) {
150865
151099
  }
150866
151100
  case "session.text.delta": {
150867
151101
  const data = event.data;
151102
+ deps.log.debug("text.delta", {
151103
+ sessionID: data.sessionID,
151104
+ msg: data.assistantMessageID,
151105
+ len: typeof data.delta === "string" ? data.delta.length : 0
151106
+ });
150868
151107
  deps.applyRun(data.sessionID, {
150869
151108
  type: "text.delta",
150870
151109
  delta: data.delta,
@@ -150874,6 +151113,12 @@ async function routeEvent(event, deps) {
150874
151113
  }
150875
151114
  case "session.text.ended": {
150876
151115
  const data = event.data;
151116
+ deps.log.debug("text.ended", {
151117
+ sessionID: data.sessionID,
151118
+ msg: data.assistantMessageID,
151119
+ len: typeof data.text === "string" ? data.text.length : -1,
151120
+ hasText: typeof data.text === "string"
151121
+ });
150877
151122
  deps.applyRun(data.sessionID, {
150878
151123
  type: "text.ended",
150879
151124
  ...data.text ? { text: data.text } : {},
@@ -150916,18 +151161,21 @@ async function routeEvent(event, deps) {
150916
151161
  }
150917
151162
  case "session.execution.started": {
150918
151163
  const data = event.data;
151164
+ deps.log.debug("execution.started", { sessionID: data.sessionID });
150919
151165
  deps.markStarted(data.sessionID);
150920
151166
  deps.applyRun(data.sessionID, { type: "execution.started" });
150921
151167
  break;
150922
151168
  }
150923
151169
  case "session.execution.succeeded": {
150924
151170
  const data = event.data;
151171
+ deps.log.debug("execution.succeeded", { sessionID: data.sessionID });
150925
151172
  deps.markEnded(data.sessionID);
150926
151173
  deps.applyRun(data.sessionID, { type: "execution.succeeded" });
150927
151174
  break;
150928
151175
  }
150929
151176
  case "session.execution.failed": {
150930
151177
  const data = event.data;
151178
+ deps.log.debug("execution.failed", { sessionID: data.sessionID });
150931
151179
  deps.markEnded(data.sessionID);
150932
151180
  deps.applyRun(data.sessionID, { type: "execution.failed", error: extractErrorText(data.error) });
150933
151181
  void deps.notifyFailure(data.sessionID, data.error);
@@ -150935,6 +151183,7 @@ async function routeEvent(event, deps) {
150935
151183
  }
150936
151184
  case "session.execution.interrupted": {
150937
151185
  const data = event.data;
151186
+ deps.log.debug("execution.interrupted", { sessionID: data.sessionID, reason: data.reason });
150938
151187
  deps.markEnded(data.sessionID);
150939
151188
  deps.applyRun(data.sessionID, { type: "execution.failed", error: `\u5DF2\u4E2D\u65AD\uFF08${data.reason ?? "unknown"}\uFF09` });
150940
151189
  break;
@@ -150942,6 +151191,7 @@ async function routeEvent(event, deps) {
150942
151191
  case "session.status": {
150943
151192
  const data = event.data;
150944
151193
  const statusType = data.status?.type;
151194
+ deps.log.debug("session.status", { sessionID: data.sessionID, status: statusType });
150945
151195
  if (statusType === "idle") {
150946
151196
  deps.markEnded(data.sessionID);
150947
151197
  deps.applyRun(data.sessionID, { type: "execution.succeeded" });
@@ -150952,6 +151202,7 @@ async function routeEvent(event, deps) {
150952
151202
  }
150953
151203
  case "session.idle": {
150954
151204
  const data = event.data;
151205
+ deps.log.debug("session.idle", { sessionID: data.sessionID });
150955
151206
  deps.markEnded(data.sessionID);
150956
151207
  deps.applyRun(data.sessionID, { type: "execution.succeeded" });
150957
151208
  break;
@@ -151269,7 +151520,8 @@ function routeCardAction(action, deps) {
151269
151520
  return deps.handleCompact(action);
151270
151521
  }
151271
151522
  const hasForm = action.formValue !== void 0;
151272
- const routed = hasForm || isSetupFormAction(value3) || Boolean(parseSessionCardValue(value3)) || Boolean(parseSetupCardValue(value3));
151523
+ const routed = hasForm || isSetupFormAction(value3) || Boolean(parseSessionCardValue(value3)) || Boolean(parseSetupCardValue(value3)) || // 「一句话建会话」建议卡按钮(issue #2)。
151524
+ Boolean(parseQuickNewActionValue(value3));
151273
151525
  deps.log.debug("\u5361\u7247\u56DE\u8C03\u8DEF\u7531", {
151274
151526
  hasForm,
151275
151527
  hasValue: value3 !== void 0,
@@ -152031,6 +152283,10 @@ async function start(ctx, config, log2, logSink) {
152031
152283
  rootKnown: Boolean(rootLink)
152032
152284
  });
152033
152285
  if (decision.kind === "main-hint") {
152286
+ if (config.quickNew) {
152287
+ await handleQuickNew(message);
152288
+ return;
152289
+ }
152034
152290
  const res = await sender.sendCard(message.chatId, buildConsoleHintCard());
152035
152291
  if (!res.ok) log2.warn("\u7BA1\u7406\u53F0\u63D0\u793A\u5361\u53D1\u9001\u5931\u8D25", { error: res.error ?? "unknown" });
152036
152292
  return;
@@ -152126,6 +152382,242 @@ async function start(ctx, config, log2, logSink) {
152126
152382
  log2.info("\u673A\u5668\u4EBA\u83DC\u5355\u70B9\u51FB \u2192 \u6267\u884C\u547D\u4EE4", { eventKey: click.eventKey, cmd: entry.cmd, label: entry.label });
152127
152383
  await handleMessage(synthetic);
152128
152384
  }
152385
+ const QUICK_NEW_KEY_PREFIX = "feishu:v2:quicknew:";
152386
+ const QUICK_NEW_TTL_MS = 60 * 60 * 1e3;
152387
+ function parseQuickNewEntry(raw) {
152388
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
152389
+ const v = raw;
152390
+ const text = typeof v.text === "string" ? v.text : "";
152391
+ const dir = typeof v.dir === "string" ? v.dir : "";
152392
+ const chatId = typeof v.chatId === "string" ? v.chatId : "";
152393
+ const openId = typeof v.openId === "string" ? v.openId : "";
152394
+ const anchorMessageId = typeof v.anchorMessageId === "string" ? v.anchorMessageId : "";
152395
+ if (!text || !dir || !chatId || !openId || !anchorMessageId) return void 0;
152396
+ return {
152397
+ text,
152398
+ title: typeof v.title === "string" ? v.title : "",
152399
+ dir,
152400
+ chatId,
152401
+ openId,
152402
+ anchorMessageId,
152403
+ createdAt: typeof v.createdAt === "number" ? v.createdAt : 0
152404
+ };
152405
+ }
152406
+ async function handleQuickNew(message) {
152407
+ const hint = async () => {
152408
+ const res = await sender.sendCard(message.chatId, buildConsoleHintCard());
152409
+ if (!res.ok) log2.warn("\u7BA1\u7406\u53F0\u63D0\u793A\u5361\u53D1\u9001\u5931\u8D25", { error: res.error ?? "unknown" });
152410
+ };
152411
+ try {
152412
+ const candidates = [];
152413
+ const seen = /* @__PURE__ */ new Set();
152414
+ for (const dir2 of await recent.listDirs()) {
152415
+ if (!seen.has(dir2)) {
152416
+ seen.add(dir2);
152417
+ candidates.push({ path: dir2 });
152418
+ }
152419
+ }
152420
+ let entries3 = [];
152421
+ try {
152422
+ const raw = await listAllSessionsRaw() ?? await listSessionsOverHttp({}, { log: log2 });
152423
+ entries3 = normalizeSessionList(raw) ?? [];
152424
+ } catch (err2) {
152425
+ log2.debug("quick-new \u4F1A\u8BDD\u5217\u8868\u8BFB\u53D6\u5931\u8D25", { error: errorMessage(err2) });
152426
+ }
152427
+ for (const entry2 of entries3) {
152428
+ if (entry2.directory && !seen.has(entry2.directory)) {
152429
+ seen.add(entry2.directory);
152430
+ candidates.push({ path: entry2.directory, ...entry2.title ? { label: entry2.title } : {} });
152431
+ }
152432
+ }
152433
+ const routingSessionID = entries3[0]?.sessionID;
152434
+ if (candidates.length === 0 || !routingSessionID) {
152435
+ log2.debug("quick-new \u65E0\u5019\u9009\u76EE\u5F55\u6216\u4F1A\u8BDD\uFF0C\u56DE\u9000\u63D0\u793A\u5361", {
152436
+ candidates: candidates.length,
152437
+ hasRouting: Boolean(routingSessionID)
152438
+ });
152439
+ await hint();
152440
+ return;
152441
+ }
152442
+ const ack = await sender.sendCard(message.chatId, buildQuickNewThinkingCard());
152443
+ if (!ack.ok || !ack.messageId) {
152444
+ await hint();
152445
+ return;
152446
+ }
152447
+ const generateText = async (prompt, requestOptions) => {
152448
+ const api = ctx.generate.text;
152449
+ if (typeof api !== "function") throw new Error("generate.text unavailable");
152450
+ return api({ prompt }, requestOptions);
152451
+ };
152452
+ const outcome = await quickGenerateWithSession(
152453
+ { log: log2, generateText },
152454
+ { prompt: buildQuickNewPrompt(message.text, candidates), sessionID: routingSessionID }
152455
+ );
152456
+ const decision = parseQuickNewDecision(extractGeneratedText(outcome.result));
152457
+ const dir = decision?.intent === "task" ? matchCandidateDirectory(decision.directory, candidates) : void 0;
152458
+ const validation = dir ? validateDir(dir) : void 0;
152459
+ if (!decision || decision.intent !== "task" || !dir || !validation?.ok) {
152460
+ log2.debug("quick-new \u5224\u5B9A\u4E3A\u975E\u4EFB\u52A1\u6216\u672A\u5339\u914D\u5230\u76EE\u5F55", {
152461
+ intent: decision?.intent,
152462
+ dir: decision?.directory,
152463
+ reason: decision?.reason
152464
+ });
152465
+ await sender.patchCard(ack.messageId, buildConsoleHintCard());
152466
+ return;
152467
+ }
152468
+ const id2 = `qn_${message.messageId}`;
152469
+ const title = decision.title || topicTitle(message.text);
152470
+ const entry = {
152471
+ text: message.text,
152472
+ title,
152473
+ dir: validation.path,
152474
+ chatId: message.chatId,
152475
+ openId: message.senderOpenId,
152476
+ anchorMessageId: message.messageId,
152477
+ createdAt: Date.now()
152478
+ };
152479
+ await ctx.storage.set(
152480
+ QUICK_NEW_KEY_PREFIX + id2,
152481
+ entry
152482
+ );
152483
+ await sender.patchCard(
152484
+ ack.messageId,
152485
+ buildQuickNewProposalCard({
152486
+ id: id2,
152487
+ title,
152488
+ directory: validation.path,
152489
+ textPreview: message.text,
152490
+ ...decision.reason ? { reason: decision.reason } : {}
152491
+ })
152492
+ );
152493
+ log2.info("quick-new \u5EFA\u8BAE\u5361\u5DF2\u53D1\u9001", { dir: validation.path, title, reason: decision.reason });
152494
+ } catch (err2) {
152495
+ log2.warn("quick-new \u5904\u7406\u5931\u8D25\uFF0C\u56DE\u9000\u7BA1\u7406\u53F0\u63D0\u793A", { error: errorMessage(err2) });
152496
+ await hint();
152497
+ }
152498
+ }
152499
+ async function handleQuickNewCardAction(action, value3) {
152500
+ if (!owner.isAllowed(action.operatorOpenId)) {
152501
+ return { toast: { type: "error", content: "\u65E0\u64CD\u4F5C\u6743\u9650" } };
152502
+ }
152503
+ const key = QUICK_NEW_KEY_PREFIX + value3.id;
152504
+ let raw;
152505
+ try {
152506
+ raw = await ctx.storage.get(key);
152507
+ } catch {
152508
+ raw = void 0;
152509
+ }
152510
+ const entry = parseQuickNewEntry(raw);
152511
+ if (!entry || Date.now() - entry.createdAt > QUICK_NEW_TTL_MS) {
152512
+ try {
152513
+ await ctx.storage.remove(key);
152514
+ } catch {
152515
+ }
152516
+ if (action.messageId) {
152517
+ await sender.patchCard(
152518
+ action.messageId,
152519
+ buildQuickNewResolvedCard("\u231B\uFE0F \u5EFA\u8BAE\u5DF2\u8FC7\u671F", "\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\u3002", "orange")
152520
+ );
152521
+ }
152522
+ return { toast: { type: "warning", content: "\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u53D1\u6D88\u606F" } };
152523
+ }
152524
+ if (value3.op === "cancel") {
152525
+ try {
152526
+ await ctx.storage.remove(key);
152527
+ } catch {
152528
+ }
152529
+ if (action.messageId) {
152530
+ await sender.patchCard(action.messageId, buildQuickNewResolvedCard("\u274C \u5DF2\u53D6\u6D88", "\u672A\u521B\u5EFA\u4F1A\u8BDD\u3002"));
152531
+ }
152532
+ return { toast: { type: "info", content: "\u5DF2\u53D6\u6D88" } };
152533
+ }
152534
+ const validation = validateDir(entry.dir);
152535
+ if (!validation.ok) {
152536
+ if (action.messageId) {
152537
+ await sender.patchCard(
152538
+ action.messageId,
152539
+ buildQuickNewResolvedCard("\u26A0\uFE0F \u76EE\u5F55\u4E0D\u53EF\u7528", `${entry.dir}\uFF1A${validation.message}`, "red")
152540
+ );
152541
+ }
152542
+ return { toast: { type: "error", content: "\u76EE\u5F55\u4E0D\u53EF\u7528" } };
152543
+ }
152544
+ try {
152545
+ await ctx.storage.remove(key);
152546
+ } catch {
152547
+ }
152548
+ const perm = "edit";
152549
+ const title = entry.title || topicTitle(entry.text);
152550
+ void (async () => {
152551
+ const created = await createSessionInternal({
152552
+ title,
152553
+ chatId: entry.chatId,
152554
+ openId: entry.openId,
152555
+ setActive: false,
152556
+ directory: validation.path,
152557
+ permissions: presetToRuleset(perm),
152558
+ perm,
152559
+ gateMode: presetGateMode(perm)
152560
+ });
152561
+ await recent.addDir(validation.path);
152562
+ const readyCard = buildSessionReadyCard({
152563
+ title,
152564
+ sessionID: created.id,
152565
+ dir: validation.path,
152566
+ perm: presetLabel(perm)
152567
+ });
152568
+ const res = await sender.replyCard(entry.anchorMessageId, readyCard, { replyInThread: true });
152569
+ if (res.ok && res.messageId) {
152570
+ await sessionMap.bindRoot(entry.anchorMessageId, created.id);
152571
+ const meta = res.threadId ? void 0 : await sender.getMessageMeta(res.messageId);
152572
+ const threadId = res.threadId ?? meta?.threadId;
152573
+ if (threadId) {
152574
+ await sessionMap.bindThread(threadId, created.id, entry.chatId, entry.openId, entry.anchorMessageId);
152575
+ } else {
152576
+ log2.warn("quick-new \u5F00\u8BDD\u9898\u540E\u672A\u8BFB\u5230 thread_id", { sessionID: created.id });
152577
+ }
152578
+ await sessionMap.setRootCard(created.id, {
152579
+ style: "created",
152580
+ sessionID: created.id,
152581
+ title,
152582
+ dir: validation.path,
152583
+ perm: presetLabel(perm)
152584
+ });
152585
+ } else {
152586
+ log2.warn("quick-new \u5F00\u8BDD\u9898\u5931\u8D25", { sessionID: created.id, error: res.error ?? "unknown" });
152587
+ }
152588
+ await runInSession(
152589
+ {
152590
+ eventId: `quicknew_${value3.id}`,
152591
+ messageId: entry.anchorMessageId,
152592
+ chatId: entry.chatId,
152593
+ text: entry.text,
152594
+ senderOpenId: entry.openId
152595
+ },
152596
+ created.id,
152597
+ entry.anchorMessageId
152598
+ );
152599
+ if (action.messageId) {
152600
+ await sender.patchCard(
152601
+ action.messageId,
152602
+ buildQuickNewResolvedCard(
152603
+ "\u2705 \u5DF2\u521B\u5EFA",
152604
+ `\u76EE\u5F55\uFF1A\`${validation.path}\`
152605
+ \u5DF2\u5F00\u8BDD\u9898\u5E76\u5F00\u59CB\u5904\u7406\u4F60\u7684\u6D88\u606F\u3002`,
152606
+ "green"
152607
+ )
152608
+ );
152609
+ }
152610
+ })().catch((err2) => {
152611
+ log2.warn("quick-new \u5EFA\u4F1A\u8BDD\u5931\u8D25", { error: errorMessage(err2) });
152612
+ if (action.messageId) {
152613
+ void sender.patchCard(
152614
+ action.messageId,
152615
+ buildQuickNewResolvedCard("\u26A0\uFE0F \u521B\u5EFA\u5931\u8D25", errorMessage(err2).slice(0, 160), "red")
152616
+ );
152617
+ }
152618
+ });
152619
+ return { toast: { type: "success", content: "\u5DF2\u521B\u5EFA\uFF0C\u6B63\u5728\u6253\u5F00\u8BDD\u9898\u2026" } };
152620
+ }
152129
152621
  async function runInActiveSession(message) {
152130
152622
  let active = await sessionMap.getActive(message.chatId);
152131
152623
  if (!active) {
@@ -152200,7 +152692,11 @@ ${downloadedAttachmentPrompt(message.attachment, outcome)}`;
152200
152692
  handleForm: (a) => formRelay.handleCardAction(a),
152201
152693
  handleStop: (a) => stop.handleCardAction(a),
152202
152694
  handleCompact: (a) => compact2.handleCardAction(a),
152203
- handleCommands: (a) => commands.handleCardAction(a),
152695
+ handleCommands: (a) => {
152696
+ const quick = parseQuickNewActionValue(a.rawValue);
152697
+ if (quick) return handleQuickNewCardAction(a, quick);
152698
+ return commands.handleCardAction(a);
152699
+ },
152204
152700
  handleApprovals: (a) => approvals.handleCardAction(a)
152205
152701
  })
152206
152702
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-feishu-plugin",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "Feishu/Lark plugin for OpenCode V2 — single-user topics as sessions, in-card permission approvals, streaming cards. Minimal permissions, no public endpoint.",
5
5
  "type": "module",
6
6
  "main": "index.js",